From 86b1ae5247b0732b647e62715a9c5304c59a3121 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sat, 22 Feb 2025 17:19:56 +0000 Subject: [PATCH 01/20] Reduced the type: ignore comments and allocated the structured dataclass --- odxtools/cli/compare.py | 125 ++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 64 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 609344c9..855c826b 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -3,7 +3,7 @@ import argparse import os -from typing import Any, Dict, List, Optional, Set, Union, cast +from typing import TypedDict, Any, Dict, List, Optional, Set, Union, cast, Tuple from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -23,11 +23,13 @@ from ._parser_utils import SubparsersList from ._print_utils import (extract_service_tabulation_data, print_dl_metrics, print_service_parameters) +from dataclasses import dataclass # name of the tool _odxtools_tool_name_ = "compare" - +""" VariantName = str + VariantType = str NewServices = List[DiagService] DeletedServices = List[DiagService] @@ -41,76 +43,79 @@ DeletedVariants = List[DiagLayer] SpecsChangesVariants = Dict[str, Union[NewVariants, DeletedVariants, SpecsServiceDict]] - - +""" + + +class SpecsServiceDict(TypedDict): + diag_layer: str + diag_layer_type: str + new_services: List[str] # List of new service names + deleted_services: List[DiagService] # List of deleted services + renamed_service: List[Tuple[str, DiagService]] # List of (old_name, DiagService) + changed_name_of_service: List[Tuple[str, DiagService]] # Similar to renamed_service + changed_parameters_of_service: List[Tuple[str, DiagService, List[Union[str, int, float]]]] +class SpecsChangesVariants(TypedDict): + new_variants : List[str] + deleted_variants : List[str] + service_changes : SpecsServiceDict +#class extract_service_tabulation_data(TypedDict): +# table: RichTable + + +@dataclass class Display: - # class with variables and functions to display the result of the comparison - - # TODO - # - Idea: results as json export - # - write results of comparison in json structure - # - use odxlinks to refer to dignostic services / objects if - # changes have already been detected (e.g. in another ecu - # variant / diagnostic layer) - # - print all information about parameter properties (request, - # pos. response & neg. response parameters) for changed diagnostic - # services - param_detailed: bool - obj_detailed: bool - - def __init__(self) -> None: - pass + param_detailed: bool = False + obj_detailed: bool = False def print_dl_changes(self, service_dict: SpecsServiceDict) -> None: if service_dict["new_services"] or service_dict["deleted_services"] or service_dict[ - "changed_name_of_service"][0] or service_dict["changed_parameters_of_service"][0]: - assert isinstance(service_dict["diag_layer"], str) + "changed_name_of_service"] or service_dict["changed_parameters_of_service"]: + # assert isinstance(service_dict["diag_layer"], str) rich_print() rich_print( f"Changed diagnostic services for diagnostic layer '{service_dict['diag_layer']}' ({service_dict['diag_layer_type']}):" ) if service_dict["new_services"]: - assert isinstance(service_dict["new_services"], List) + # assert isinstance(service_dict["new_services"], List) rich_print() rich_print(" [blue]New services[/blue]") rich_print(extract_service_tabulation_data( - service_dict["new_services"])) # type: ignore[arg-type] + service_dict["new_services"])) + if service_dict["deleted_services"]: - assert isinstance(service_dict["deleted_services"], List) + # assert isinstance(service_dict["deleted_services"], List) rich_print() rich_print(" [blue]Deleted services[/blue]") rich_print(extract_service_tabulation_data( - service_dict["deleted_services"])) # type: ignore[arg-type] + service_dict["deleted_services"])) if service_dict["changed_name_of_service"][0]: rich_print() rich_print(" [blue]Renamed services[/blue]") rich_print(extract_service_tabulation_data( - service_dict["changed_name_of_service"][0])) # type: ignore[arg-type] + service_dict["changed_name_of_service"][0])) if service_dict["changed_parameters_of_service"][0]: rich_print() rich_print(" [blue]Services with parameter changes[/blue]") # create table with information about services with parameter changes changed_param_column = [ str(x) for x in service_dict["changed_parameters_of_service"][ - 1] # type: ignore[union-attr] + 1] ] table = extract_service_tabulation_data( - service_dict["changed_parameters_of_service"][0], # type: ignore[arg-type] + service_dict["changed_parameters_of_service"][0], additional_columns=[("Changed Parameters", changed_param_column)]) rich_print(table) for service_idx, service in enumerate( - service_dict["changed_parameters_of_service"][0]): # type: ignore[arg-type] + service_dict["changed_parameters_of_service"][0]): assert isinstance(service, DiagService) rich_print() rich_print( f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" ) # detailed_info in [infotext1, dict1, infotext2, dict2, ...] - info_list = cast( - list, # type: ignore[type-arg] - service_dict["changed_parameters_of_service"])[2][service_idx] + info_list = cast(list, service_dict["changed_parameters_of_service"])[2][service_idx] for detailed_info in info_list: if isinstance(detailed_info, str): rich_print() @@ -165,7 +170,6 @@ class Comparison(Display): db_indicator_1: int db_indicator_2: int - def __init__(self) -> None: pass @@ -280,9 +284,10 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List[SpecsServiceDict]: # compares request, positive response and negative response parameters of two diagnostic services + information: List[Union[str, Dict[str, Any]]] = [ ] # information = [infotext1, table1, infotext2, table2, ...] - changed_params = "" + changed_params: str = "" # Request if service1.request is not None and service2.request is not None and len( @@ -403,33 +408,25 @@ def compare_services(self, service1: DiagService, str(service2.negative_responses)] }) - return [information, changed_params] # type: ignore[list-item] + return [information, changed_params] def compare_diagnostic_layers(self, dl1: DiagLayer, - dl2: DiagLayer) -> dict: # type: ignore[type-arg] + dl2: DiagLayer)-> SpecsServiceDict: # compares diagnostic services of two diagnostic layers with each other # save changes in dictionary (service_dict) # TODO: add comparison of SingleECUJobs - new_services: NewServices = [] - deleted_services: DeletedServices = [] - renamed_service: RenamedServices = [[], - []] # TODO: implement list of (str, DiagService)-tuples - services_with_param_changes: ServicesWithParamChanges = [ - [], [], [] - ] # TODO: implement list of tuples (str, str, DiagService)-tuples - service_dict: SpecsServiceDict = { "diag_layer": dl1.short_name, "diag_layer_type": dl1.variant_type.value, # list with added diagnostic services [service1, service2, service3, ...] Type: DiagService - "new_services": new_services, + "new_services": [], # list with deleted diagnostic services [service1, service2, service3, ...] Type: DiagService - "deleted_services": deleted_services, + "deleted_services": [], # list with diagnostic services where the service name changed [[services], [old service names]] - "changed_name_of_service": renamed_service, + "changed_name_of_service": [[],[]], # list with diagnostic services where the service parameter changed [[services], [changed_parameters], [information_texts]] - "changed_parameters_of_service": services_with_param_changes + "changed_parameters_of_service": [[],[],[]] } # service_dict["changed_name_of_service"][{0 = services, 1 = old service names}][i] # service_dict["changed_parameters_of_service"][{0 = services, 1 = changed_parameters, 2 = information_texts}][i] @@ -457,7 +454,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - service_dict["new_services"].append( # type: ignore[union-attr] + service_dict["new_services"].append( service1) # type: ignore[arg-type] # check whether names of diagnostic services have changed @@ -470,10 +467,10 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, # save information about changes in dictionary # add new service (type: DiagService) - service_dict["changed_name_of_service"][0].append( # type: ignore[union-attr] + service_dict["changed_name_of_service"][0].append( service1) # add old service name (type: String) - service_dict["changed_name_of_service"][1].append( # type: ignore[union-attr] + service_dict["changed_name_of_service"][1].append( service2.short_name) # compare request, pos. response and neg. response parameters of diagnostic services @@ -484,16 +481,16 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, if detailed_information[1]: # check whether string "changed_params" is empty # new service (type: DiagService) service_dict["changed_parameters_of_service"][ - 0].append( # type: ignore[union-attr] + 0].append( service1) # add parameters which have been changed (type: String) service_dict["changed_parameters_of_service"][ - 1].append( # type: ignore[union-attr] - detailed_information[1]) # type: ignore[arg-type] + 1].append( + detailed_information[1]) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] service_dict["changed_parameters_of_service"][ - 2].append( # type: ignore[union-attr] - detailed_information[0]) # type: ignore[arg-type] + 2].append( + detailed_information[0]) for service2_idx, service2 in enumerate(dl2.services): @@ -505,7 +502,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, assert isinstance(deleted_list, list) if service2 not in deleted_list: service_dict["deleted_services"].append( # type: ignore[union-attr] - service2) # type: ignore[arg-type] + service2) if service1.short_name == service2.short_name: # compare request, pos. response and neg. response parameters of both diagnostic services @@ -516,22 +513,22 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, if detailed_information[1]: # check whether string "changed_params" is empty # new service (type: DiagService) service_dict["changed_parameters_of_service"][ - 0].append( # type: ignore[union-attr] + 0].append( service1) # add parameters which have been changed (type: String) - service_dict["changed_parameters_of_service"][ # type: ignore[union-attr] - 1].append(detailed_information[1]) # type: ignore[arg-type] + service_dict["changed_parameters_of_service"][ + 1].append(detailed_information[1]) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] service_dict["changed_parameters_of_service"][ # type: ignore[union-attr] 2].append(detailed_information[0]) # type: ignore[arg-type] return service_dict def compare_databases(self, database_new: Database, - database_old: Database) -> dict: # type: ignore[type-arg] + database_old: Database) -> SpecsChangesVariants: # type: ignore[type-arg] # compares two PDX-files with each other - new_variants: NewVariants = [] - deleted_variants: DeletedVariants = [] + new_variants = [] + deleted_variants= [] changes_variants: SpecsChangesVariants = { "new_diagnostic_layers": new_variants, From 38dec1f66e88f9842e2b0e61d7401a23633a21c4 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sat, 22 Feb 2025 17:23:16 +0000 Subject: [PATCH 02/20] Reduced the type: ignore comments and allocated the structured dataclass --- odxtools/cli/compare.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 855c826b..db25e522 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -27,23 +27,7 @@ # name of the tool _odxtools_tool_name_ = "compare" -""" -VariantName = str -VariantType = str -NewServices = List[DiagService] -DeletedServices = List[DiagService] -RenamedServices = List[List[Union[str, DiagService]]] -ServicesWithParamChanges = List[List[Union[str, DiagService]]] - -SpecsServiceDict = Dict[str, Union[VariantName, VariantType, NewServices, DeletedServices, - RenamedServices, ServicesWithParamChanges]] - -NewVariants = List[DiagLayer] -DeletedVariants = List[DiagLayer] - -SpecsChangesVariants = Dict[str, Union[NewVariants, DeletedVariants, SpecsServiceDict]] -""" class SpecsServiceDict(TypedDict): @@ -58,8 +42,6 @@ class SpecsChangesVariants(TypedDict): new_variants : List[str] deleted_variants : List[str] service_changes : SpecsServiceDict -#class extract_service_tabulation_data(TypedDict): -# table: RichTable @dataclass From 824a499edb5da01b610a448fd8379156eb23d9e2 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sat, 22 Feb 2025 23:57:57 +0000 Subject: [PATCH 03/20] fixed the lint issues --- odxtools/cli/compare.py | 562 ++++++++++++++++++++++++++-------------- 1 file changed, 367 insertions(+), 195 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index db25e522..d5d64117 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -3,7 +3,9 @@ import argparse import os -from typing import TypedDict, Any, Dict, List, Optional, Set, Union, cast, Tuple +from dataclasses import dataclass +from typing import (Any, Dict, List, Optional, Set, Tuple, TypedDict, Union, + cast) from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -23,13 +25,11 @@ from ._parser_utils import SubparsersList from ._print_utils import (extract_service_tabulation_data, print_dl_metrics, print_service_parameters) -from dataclasses import dataclass # name of the tool _odxtools_tool_name_ = "compare" - class SpecsServiceDict(TypedDict): diag_layer: str diag_layer_type: str @@ -37,11 +37,15 @@ class SpecsServiceDict(TypedDict): deleted_services: List[DiagService] # List of deleted services renamed_service: List[Tuple[str, DiagService]] # List of (old_name, DiagService) changed_name_of_service: List[Tuple[str, DiagService]] # Similar to renamed_service - changed_parameters_of_service: List[Tuple[str, DiagService, List[Union[str, int, float]]]] + changed_parameters_of_service: List[ + Tuple[str, DiagService, List[Union[str, int, float]]] + ] + + class SpecsChangesVariants(TypedDict): - new_variants : List[str] - deleted_variants : List[str] - service_changes : SpecsServiceDict + new_variants: List[str] + deleted_variants: List[str] + service_changes: SpecsServiceDict @dataclass @@ -51,53 +55,63 @@ class Display: def print_dl_changes(self, service_dict: SpecsServiceDict) -> None: - if service_dict["new_services"] or service_dict["deleted_services"] or service_dict[ - "changed_name_of_service"] or service_dict["changed_parameters_of_service"]: - # assert isinstance(service_dict["diag_layer"], str) + if ( + service_dict["new_services"] + or service_dict["deleted_services"] + or service_dict["changed_name_of_service"] + or service_dict["changed_parameters_of_service"] + ): + # assert isinstance(service_dict["diag_layer"], str) rich_print() rich_print( f"Changed diagnostic services for diagnostic layer '{service_dict['diag_layer']}' ({service_dict['diag_layer_type']}):" ) if service_dict["new_services"]: - # assert isinstance(service_dict["new_services"], List) + # assert isinstance(service_dict["new_services"], List) rich_print() rich_print(" [blue]New services[/blue]") - rich_print(extract_service_tabulation_data( - service_dict["new_services"])) - + rich_print(extract_service_tabulation_data(service_dict["new_services"])) + if service_dict["deleted_services"]: - # assert isinstance(service_dict["deleted_services"], List) + # assert isinstance(service_dict["deleted_services"], List) rich_print() rich_print(" [blue]Deleted services[/blue]") - rich_print(extract_service_tabulation_data( - service_dict["deleted_services"])) + rich_print( + extract_service_tabulation_data(service_dict["deleted_services"]) + ) if service_dict["changed_name_of_service"][0]: rich_print() rich_print(" [blue]Renamed services[/blue]") - rich_print(extract_service_tabulation_data( - service_dict["changed_name_of_service"][0])) + rich_print( + extract_service_tabulation_data( + service_dict["changed_name_of_service"][0] + ) + ) if service_dict["changed_parameters_of_service"][0]: rich_print() rich_print(" [blue]Services with parameter changes[/blue]") # create table with information about services with parameter changes changed_param_column = [ - str(x) for x in service_dict["changed_parameters_of_service"][ - 1] + str(x) for x in service_dict["changed_parameters_of_service"][1] ] table = extract_service_tabulation_data( - service_dict["changed_parameters_of_service"][0], - additional_columns=[("Changed Parameters", changed_param_column)]) + service_dict["changed_parameters_of_service"][0], + additional_columns=[("Changed Parameters", changed_param_column)], + ) rich_print(table) for service_idx, service in enumerate( - service_dict["changed_parameters_of_service"][0]): + service_dict["changed_parameters_of_service"][0] + ): assert isinstance(service, DiagService) rich_print() rich_print( f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" ) # detailed_info in [infotext1, dict1, infotext2, dict2, ...] - info_list = cast(list, service_dict["changed_parameters_of_service"])[2][service_idx] + info_list = cast(list, service_dict["changed_parameters_of_service"])[ + 2 + ][service_idx] for detailed_info in info_list: if isinstance(detailed_info, str): rich_print() @@ -107,7 +121,8 @@ def print_dl_changes(self, service_dict: SpecsServiceDict) -> None: show_header=True, header_style="bold cyan", border_style="blue", - show_lines=True) + show_lines=True, + ) for header in detailed_info: table.add_column(header) rows = zip(*detailed_info.values()) @@ -124,20 +139,24 @@ def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None # prints result of database comparison (input variable: dictionary: changes_variants) # diagnostic layers - if changes_variants["new_diagnostic_layers"] or changes_variants[ - "deleted_diagnostic_layers"]: + if ( + changes_variants["new_diagnostic_layers"] + or changes_variants["deleted_diagnostic_layers"] + ): rich_print() rich_print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") rich_print(" New diagnostic layers: ") for variant in changes_variants["new_diagnostic_layers"]: assert isinstance(variant, DiagLayer) rich_print( - f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})" + ) rich_print(" Deleted diagnostic layers: ") for variant in changes_variants["deleted_diagnostic_layers"]: assert isinstance(variant, DiagLayer) rich_print( - f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})" + ) # diagnostic services for _, value in changes_variants.items(): @@ -152,10 +171,13 @@ class Comparison(Display): db_indicator_1: int db_indicator_2: int + def __init__(self) -> None: pass - def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, Any]: + def compare_parameters( + self, param1: Parameter, param2: Parameter + ) -> Dict[str, Any]: # checks whether properties of param1 and param2 differ # checked properties: Name, Byte Position, Bit Length, Semantic, Parameter Type, Value (Coded, Constant, Default etc.), Data Type, Data Object Property (Name, Physical Data Type, Unit) @@ -163,8 +185,11 @@ def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, old = [] new = [] - def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], - old_property_value: Optional[AtomicOdxType]) -> None: + def append_list( + property_name: str, + new_property_value: Optional[AtomicOdxType], + old_property_value: Optional[AtomicOdxType], + ) -> None: property.append(property_name) old.append(old_property_value) new.append(new_property_value) @@ -174,35 +199,62 @@ def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], if param1.byte_position != param2.byte_position: append_list("Byte position", param1.byte_position, param2.byte_position) if param1.get_static_bit_length() != param2.get_static_bit_length(): - append_list("Bit Length", param1.get_static_bit_length(), - param2.get_static_bit_length()) + append_list( + "Bit Length", + param1.get_static_bit_length(), + param2.get_static_bit_length(), + ) if param1.semantic != param2.semantic: append_list("Semantic", param1.semantic, param2.semantic) if param1.parameter_type != param2.parameter_type: append_list("Parameter type", param1.parameter_type, param2.parameter_type) - if isinstance(param1, CodedConstParameter) and isinstance(param2, CodedConstParameter): - if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type: - append_list("Data type", param1.diag_coded_type.base_data_type.name, - param2.diag_coded_type.base_data_type.name) + if isinstance(param1, CodedConstParameter) and isinstance( + param2, CodedConstParameter + ): + if ( + param1.diag_coded_type.base_data_type + != param2.diag_coded_type.base_data_type + ): + append_list( + "Data type", + param1.diag_coded_type.base_data_type.name, + param2.diag_coded_type.base_data_type.name, + ) if param1.coded_value != param2.coded_value: - if isinstance(param1.coded_value, int) and isinstance(param2.coded_value, int): + if isinstance(param1.coded_value, int) and isinstance( + param2.coded_value, int + ): append_list( "Value", f"0x{param1.coded_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}") + f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}", + ) else: - append_list("Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}") + append_list( + "Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}" + ) - elif isinstance(param1, NrcConstParameter) and isinstance(param2, NrcConstParameter): - if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type: - append_list("Data type", param1.diag_coded_type.base_data_type.name, - param2.diag_coded_type.base_data_type.name) + elif isinstance(param1, NrcConstParameter) and isinstance( + param2, NrcConstParameter + ): + if ( + param1.diag_coded_type.base_data_type + != param2.diag_coded_type.base_data_type + ): + append_list( + "Data type", + param1.diag_coded_type.base_data_type.name, + param2.diag_coded_type.base_data_type.name, + ) if param1.coded_values != param2.coded_values: - append_list("Values", str(param1.coded_values), str(param2.coded_values)) + append_list( + "Values", str(param1.coded_values), str(param2.coded_values) + ) - elif (dop_1 := getattr(param1, "dop", None)) is not None and (dop_2 := getattr( - param2, "dop", None)) is not None: + elif (dop_1 := getattr(param1, "dop", None)) is not None and ( + dop_2 := getattr(param2, "dop", None) + ) is not None: if dop_1 != dop_2: # TODO: compare INTERNAL-CONSTR, COMPU-INTERNAL-TO-PHYS of DOP @@ -215,93 +267,140 @@ def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], # DOP Unit if getattr(dop_1, "unit", None) and getattr(dop_2, "unit", None): # (properties of unit object: short_name, long_name, description, odx_id, display_name, oid, factor_si_to_unit, offset_si_to_unit, physical_dimension_ref) - if dop_1.unit != dop_2.unit and dop_1.unit.short_name != dop_2.unit.short_name: - append_list(" DOP unit name", dop_1.unit.short_name, dop_2.unit.short_name) - elif dop_1.unit != dop_2.unit and dop_1.unit.display_name != dop_2.unit.display_name: - append_list(" DOP unit display name", dop_1.unit.display_name, - dop_2.unit.display_name) + if ( + dop_1.unit != dop_2.unit + and dop_1.unit.short_name != dop_2.unit.short_name + ): + append_list( + " DOP unit name", + dop_1.unit.short_name, + dop_2.unit.short_name, + ) + elif ( + dop_1.unit != dop_2.unit + and dop_1.unit.display_name != dop_2.unit.display_name + ): + append_list( + " DOP unit display name", + dop_1.unit.display_name, + dop_2.unit.display_name, + ) elif dop_1.unit != dop_2.unit: append_list(" DOP unit object", "", "") if hasattr(dop_1, "physical_type") and hasattr(dop_2, "physical_type"): - if (dop_1.physical_type and dop_2.physical_type and - dop_1.physical_type.base_data_type - != dop_2.physical_type.base_data_type): - append_list(" DOP physical data type", - dop_1.physical_type.base_data_type.name, - dop_2.physical_type.base_data_type.name) - - if (isinstance(param1, PhysicalConstantParameter) and - isinstance(param2, PhysicalConstantParameter) and - param1.physical_constant_value != param2.physical_constant_value): + if ( + dop_1.physical_type + and dop_2.physical_type + and dop_1.physical_type.base_data_type + != dop_2.physical_type.base_data_type + ): + append_list( + " DOP physical data type", + dop_1.physical_type.base_data_type.name, + dop_2.physical_type.base_data_type.name, + ) + + if ( + isinstance(param1, PhysicalConstantParameter) + and isinstance(param2, PhysicalConstantParameter) + and param1.physical_constant_value != param2.physical_constant_value + ): if isinstance(param1.physical_constant_value, int) and isinstance( - param2.physical_constant_value, int): + param2.physical_constant_value, int + ): append_list( "Constant value", f"0x{param1.physical_constant_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.physical_constant_value:0{(param2.get_static_bit_length() or 0) // 4}X}" + f"0x{param2.physical_constant_value:0{(param2.get_static_bit_length() or 0) // 4}X}", ) else: - append_list("Constant value", f"{param1.physical_constant_value!r}", - f"{param2.physical_constant_value!r}") + append_list( + "Constant value", + f"{param1.physical_constant_value!r}", + f"{param2.physical_constant_value!r}", + ) - elif (isinstance(param1, ValueParameter) and isinstance(param2, ValueParameter) and - param1.physical_default_value is not None and - param2.physical_default_value is not None and - param1.physical_default_value != param2.physical_default_value): + elif ( + isinstance(param1, ValueParameter) + and isinstance(param2, ValueParameter) + and param1.physical_default_value is not None + and param2.physical_default_value is not None + and param1.physical_default_value != param2.physical_default_value + ): if isinstance(param1.physical_default_value, int) and isinstance( - param2.physical_default_value, int): + param2.physical_default_value, int + ): append_list( "Default value", f"0x{param1.physical_default_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.physical_default_value:0{(param2.get_static_bit_length() or 0) // 4}X}" + f"0x{param2.physical_default_value:0{(param2.get_static_bit_length() or 0) // 4}X}", ) else: - append_list("Default value", f"{param1.physical_default_value!r}", - f"{param2.physical_default_value!r}") + append_list( + "Default value", + f"{param1.physical_default_value!r}", + f"{param2.physical_default_value!r}", + ) return {"Property": property, "Old Value": old, "New Value": new} - def compare_services(self, service1: DiagService, - service2: DiagService) -> List[SpecsServiceDict]: + def compare_services( + self, service1: DiagService, service2: DiagService + ) -> List[SpecsServiceDict]: # compares request, positive response and negative response parameters of two diagnostic services - - information: List[Union[str, Dict[str, Any]]] = [ - ] # information = [infotext1, table1, infotext2, table2, ...] + information: List[Union[str, Dict[str, Any]]] = ( + [] + ) # information = [infotext1, table1, infotext2, table2, ...] changed_params: str = "" # Request - if service1.request is not None and service2.request is not None and len( - service1.request.parameters) == len(service2.request.parameters): + if ( + service1.request is not None + and service2.request is not None + and len(service1.request.parameters) == len(service2.request.parameters) + ): for res1_idx, param1 in enumerate(service1.request.parameters): for res2_idx, param2 in enumerate(service2.request.parameters): if res1_idx == res2_idx: # find changed request parameter properties table = self.compare_parameters(param1, param2) - infotext = (f" Properties of request parameter '{param2.short_name}' " - f"that have changed:\n") + infotext = ( + f" Properties of request parameter '{param2.short_name}' " + f"that have changed:\n" + ) # array index starts with 0 -> param[0] is 1. service parameter if table["Property"]: information.append(infotext) information.append(table) - changed_params += f"request parameter '{param2.short_name}',\n" + changed_params += ( + f"request parameter '{param2.short_name}',\n" + ) else: changed_params += "request parameter list, " # infotext - information.append(f"List of request parameters for service '{service2.short_name}' " - f"is not identical.\n") + information.append( + f"List of request parameters for service '{service2.short_name}' " + f"is not identical.\n" + ) # table - param_list1 = [] if service1.request is None else service1.request.parameters - param_list2 = [] if service2.request is None else service2.request.parameters + param_list1 = ( + [] if service1.request is None else service1.request.parameters + ) + param_list2 = ( + [] if service2.request is None else service2.request.parameters + ) - information.append({ - "List": ["Old list", "New list"], - "Values": [f"\\{param_list1}", f"\\{param_list2}"] - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [f"\\{param_list1}", f"\\{param_list2}"], + } + ) # Positive Responses if len(service1.positive_responses) == len(service2.positive_responses): @@ -310,13 +409,16 @@ def compare_services(self, service1: DiagService, if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate(response2.parameters): + for param2_idx, param2 in enumerate( + response2.parameters + ): if param1_idx == param2_idx: # find changed positive response parameter properties table = self.compare_parameters(param1, param2) infotext = ( f" Properties of positive response parameter '{param2.short_name}' that " - f"have changed:\n") + f"have changed:\n" + ) # array index starts with 0 -> param[0] is first service parameter if table["Property"]: @@ -330,22 +432,31 @@ def compare_services(self, service1: DiagService, f"List of positive response parameters for service '{service2.short_name}' is not identical." ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [str(response1.parameters), - str(response2.parameters)] - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(response1.parameters), + str(response2.parameters), + ], + } + ) else: changed_params += "positive responses list, " # infotext information.append( - f"List of positive responses for service '{service2.short_name}' is not identical.") + f"List of positive responses for service '{service2.short_name}' is not identical." + ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [str(service1.positive_responses), - str(service2.positive_responses)] - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(service1.positive_responses), + str(service2.positive_responses), + ], + } + ) # Negative Responses if len(service1.negative_responses) == len(service2.negative_responses): @@ -354,7 +465,9 @@ def compare_services(self, service1: DiagService, if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate(response2.parameters): + for param2_idx, param2 in enumerate( + response2.parameters + ): if param1_idx == param2_idx: # find changed negative response parameter properties table = self.compare_parameters(param1, param2) @@ -372,11 +485,15 @@ def compare_services(self, service1: DiagService, f"List of positive response parameters for service '{service2.short_name}' is not identical.\n" ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [str(response1.parameters), - str(response2.parameters)] - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(response1.parameters), + str(response2.parameters), + ], + } + ) else: changed_params += "negative responses list, " # infotext @@ -384,16 +501,21 @@ def compare_services(self, service1: DiagService, f"List of positive responses for service '{service2.short_name}' is not identical.\n" ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [str(service1.negative_responses), - str(service2.negative_responses)] - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(service1.negative_responses), + str(service2.negative_responses), + ], + } + ) - return [information, changed_params] + return [information, changed_params] - def compare_diagnostic_layers(self, dl1: DiagLayer, - dl2: DiagLayer)-> SpecsServiceDict: + def compare_diagnostic_layers( + self, dl1: DiagLayer, dl2: DiagLayer + ) -> SpecsServiceDict: # compares diagnostic services of two diagnostic layers with each other # save changes in dictionary (service_dict) # TODO: add comparison of SingleECUJobs @@ -406,9 +528,9 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, # list with deleted diagnostic services [service1, service2, service3, ...] Type: DiagService "deleted_services": [], # list with diagnostic services where the service name changed [[services], [old service names]] - "changed_name_of_service": [[],[]], + "changed_name_of_service": [[], []], # list with diagnostic services where the service parameter changed [[services], [changed_parameters], [information_texts]] - "changed_parameters_of_service": [[],[],[]] + "changed_parameters_of_service": [[], [], []], } # service_dict["changed_name_of_service"][{0 = services, 1 = old service names}][i] # service_dict["changed_parameters_of_service"][{0 = services, 1 = changed_parameters, 2 = information_texts}][i] @@ -418,10 +540,12 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, # extract the constant prefixes for the requests of all # services (used for duck-typed rename detection) dl1_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() for s in dl1.services + None if s.request is None else s.request.coded_const_prefix() + for s in dl1.services ] dl2_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() for s in dl2.services + None if s.request is None else s.request.coded_const_prefix() + for s in dl2.services ] # compare diagnostic services @@ -436,8 +560,9 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - service_dict["new_services"].append( - service1) # type: ignore[arg-type] + service_dict["new_services"].append( + service1 + ) # type: ignore[arg-type] # check whether names of diagnostic services have changed elif service1 not in dl2.services: @@ -449,42 +574,47 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, # save information about changes in dictionary # add new service (type: DiagService) - service_dict["changed_name_of_service"][0].append( - service1) + service_dict["changed_name_of_service"][0].append(service1) # add old service name (type: String) - service_dict["changed_name_of_service"][1].append( - service2.short_name) + service_dict["changed_name_of_service"][1].append( + service2.short_name + ) # compare request, pos. response and neg. response parameters of diagnostic services detailed_information = self.compare_services(service1, service2) # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[1]: # check whether string "changed_params" is empty + if detailed_information[ + 1 + ]: # check whether string "changed_params" is empty # new service (type: DiagService) - service_dict["changed_parameters_of_service"][ - 0].append( - service1) + service_dict["changed_parameters_of_service"][0].append( + service1 + ) # add parameters which have been changed (type: String) - service_dict["changed_parameters_of_service"][ - 1].append( - detailed_information[1]) + service_dict["changed_parameters_of_service"][1].append( + detailed_information[1] + ) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] - service_dict["changed_parameters_of_service"][ - 2].append( - detailed_information[0]) + service_dict["changed_parameters_of_service"][2].append( + detailed_information[0] + ) for service2_idx, service2 in enumerate(dl2.services): # check for deleted diagnostic services - if service2.short_name not in dl1_service_names and dl2_request_prefixes[ - service2_idx] not in dl1_request_prefixes: + if ( + service2.short_name not in dl1_service_names + and dl2_request_prefixes[service2_idx] not in dl1_request_prefixes + ): deleted_list = service_dict["deleted_services"] assert isinstance(deleted_list, list) if service2 not in deleted_list: service_dict["deleted_services"].append( # type: ignore[union-attr] - service2) + service2 + ) if service1.short_name == service2.short_name: # compare request, pos. response and neg. response parameters of both diagnostic services @@ -492,29 +622,36 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[1]: # check whether string "changed_params" is empty + if detailed_information[ + 1 + ]: # check whether string "changed_params" is empty # new service (type: DiagService) - service_dict["changed_parameters_of_service"][ - 0].append( - service1) + service_dict["changed_parameters_of_service"][0].append( + service1 + ) # add parameters which have been changed (type: String) - service_dict["changed_parameters_of_service"][ - 1].append(detailed_information[1]) + service_dict["changed_parameters_of_service"][1].append( + detailed_information[1] + ) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] service_dict["changed_parameters_of_service"][ # type: ignore[union-attr] - 2].append(detailed_information[0]) # type: ignore[arg-type] + 2 + ].append( + detailed_information[0] + ) # type: ignore[arg-type] return service_dict - def compare_databases(self, database_new: Database, - database_old: Database) -> SpecsChangesVariants: # type: ignore[type-arg] + def compare_databases( + self, database_new: Database, database_old: Database + ) -> SpecsChangesVariants: # type: ignore[type-arg] # compares two PDX-files with each other new_variants = [] - deleted_variants= [] + deleted_variants = [] changes_variants: SpecsChangesVariants = { "new_diagnostic_layers": new_variants, - "deleted_diagnostic_layers": deleted_variants + "deleted_diagnostic_layers": deleted_variants, } # compare databases @@ -525,17 +662,27 @@ def compare_databases(self, database_new: Database, for _, dl2 in enumerate(database_old.diag_layers): # check for deleted diagnostic layers - if (dl2.short_name not in [dl.short_name for dl in database_new.diag_layers] and - dl2 not in changes_variants["deleted_diagnostic_layers"]): + if ( + dl2.short_name + not in [dl.short_name for dl in database_new.diag_layers] + and dl2 not in changes_variants["deleted_diagnostic_layers"] + ): changes_variants[ - "deleted_diagnostic_layers"].append( # type: ignore[union-attr] - dl2) + "deleted_diagnostic_layers" + ].append( # type: ignore[union-attr] + dl2 + ) - if dl1.short_name == dl2.short_name and dl1.short_name in self.diagnostic_layer_names: + if ( + dl1.short_name == dl2.short_name + and dl1.short_name in self.diagnostic_layer_names + ): # compare diagnostic services of both diagnostic layers # save diagnostic service changes in dictionary (empty if no changes) - service_dict: SpecsServiceDict = self.compare_diagnostic_layers(dl1, dl2) + service_dict: SpecsServiceDict = self.compare_diagnostic_layers( + dl1, dl2 + ) if service_dict: # adds information about diagnostic service changes to return variable (changes_variants) changes_variants.update({dl1.short_name: service_dict}) @@ -546,17 +693,19 @@ def compare_databases(self, database_new: Database, def add_subparser(subparsers: SubparsersList) -> None: parser = subparsers.add_parser( "compare", - description="\n".join([ - "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", - "", - "Examples:", - " Comparison of two diagnostic layers:", - " odxtools compare ./path/to/database.pdx -v variant1 variant2", - " Comparison of two database versions:", - " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", - " For more information use:", - " odxtools compare -h", - ]), + description="\n".join( + [ + "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", + "", + "Examples:", + " Comparison of two diagnostic layers:", + " odxtools compare ./path/to/database.pdx -v variant1 variant2", + " Comparison of two database versions:", + " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", + " For more information use:", + " odxtools compare -h", + ] + ), help="Compares two versions of diagnostic layers and/or databases with each other. Checks whether diagnostic services and its parameters have changed.", formatter_class=argparse.RawTextHelpFormatter, ) @@ -603,7 +752,9 @@ def run(args: argparse.Namespace) -> None: task = Comparison() task.param_detailed = args.no_details - db_names = [args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0])] + db_names = [ + args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0]) + ] if args.database and args.variants: # compare specified databases, consider only specified variants @@ -612,7 +763,9 @@ def run(args: argparse.Namespace) -> None: db_names.append(name) if isinstance(name, str) else str(name[0]) task.databases = [load_file(name) for name in db_names] - diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} + diag_layer_names = { + dl.short_name for db in task.databases for dl in db.diag_layers + } task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) @@ -633,22 +786,32 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") - print_dl_metrics([ - variant for variant in task.databases[0].diag_layers - if variant.short_name in task.diagnostic_layer_names - ]) + rich_print( + f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" + ) + print_dl_metrics( + [ + variant + for variant in task.databases[0].diag_layers + if variant.short_name in task.diagnostic_layer_names + ] + ) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") - print_dl_metrics([ - variant for variant in task.databases[db_idx + 1].diag_layers - if variant.short_name in task.diagnostic_layer_names - ]) + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})" + ) + print_dl_metrics( + [ + variant + for variant in task.databases[db_idx + 1].diag_layers + if variant.short_name in task.diagnostic_layer_names + ] + ) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1])) + task.compare_databases(task.databases[0], task.databases[db_idx + 1]) + ) elif args.database: # compare specified databases, consider all variants @@ -659,9 +822,7 @@ def run(args: argparse.Namespace) -> None: # collect all diagnostic layers from all specified databases task.diagnostic_layer_names = { - dl.short_name - for db in task.databases - for dl in db.diag_layers + dl.short_name for db in task.databases for dl in db.diag_layers } task.db_indicator_1 = 0 @@ -675,16 +836,20 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") + rich_print( + f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" + ) print_dl_metrics(list(task.databases[0].diag_layers)) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})" + ) print_dl_metrics(list(task.databases[db_idx + 1].diag_layers)) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1])) + task.compare_databases(task.databases[0], task.databases[db_idx + 1]) + ) elif args.variants: # no databases specified -> comparison of diagnostic layers @@ -692,11 +857,15 @@ def run(args: argparse.Namespace) -> None: odxdb = _parser_utils.load_file(args) task.databases = [odxdb] - diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} + diag_layer_names = { + dl.short_name for db in task.databases for dl in db.diag_layers + } task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) task.diagnostic_layers = [ - dl for db in task.databases for dl in db.diag_layers + dl + for db in task.databases + for dl in db.diag_layers if dl.short_name in task.diagnostic_layer_names ] @@ -714,12 +883,15 @@ def run(args: argparse.Namespace) -> None: break rich_print() - rich_print(f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})") + rich_print( + f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})" + ) rich_print( f" (compared to '{task.diagnostic_layers[db_idx + 1].short_name}' ({task.diagnostic_layers[db_idx + 1].variant_type.value}))" ) task.print_dl_changes( - task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1])) + task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1]) + ) else: # no databases & no variants specified From 716acc589dc10516b78d0350094eb21fc01f2a66 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Mon, 24 Feb 2025 21:52:47 +0000 Subject: [PATCH 04/20] fixed the issues : change the name to service_spec, executed the reformat-source.sh etc. --- odxtools/cli/compare.py | 474 ++++++++++++++-------------------------- 1 file changed, 167 insertions(+), 307 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index d5d64117..80692169 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -4,8 +4,7 @@ import argparse import os from dataclasses import dataclass -from typing import (Any, Dict, List, Optional, Set, Tuple, TypedDict, Union, - cast) +from typing import Any, Dict, List, Optional, Set, Tuple, TypedDict, Union, cast from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -30,88 +29,78 @@ _odxtools_tool_name_ = "compare" -class SpecsServiceDict(TypedDict): +@dataclass +class ServiceSpecs: diag_layer: str diag_layer_type: str new_services: List[str] # List of new service names deleted_services: List[DiagService] # List of deleted services renamed_service: List[Tuple[str, DiagService]] # List of (old_name, DiagService) changed_name_of_service: List[Tuple[str, DiagService]] # Similar to renamed_service - changed_parameters_of_service: List[ - Tuple[str, DiagService, List[Union[str, int, float]]] - ] + changed_parameters_of_service: List[Tuple[str, DiagService, List[Union[str, int, float]]]] -class SpecsChangesVariants(TypedDict): +@dataclass +class SpecsChangesVariants: new_variants: List[str] deleted_variants: List[str] - service_changes: SpecsServiceDict + service_changes: ServiceSpecs -@dataclass class Display: param_detailed: bool = False obj_detailed: bool = False - def print_dl_changes(self, service_dict: SpecsServiceDict) -> None: + def __init__(self) -> None: + pass - if ( - service_dict["new_services"] - or service_dict["deleted_services"] - or service_dict["changed_name_of_service"] - or service_dict["changed_parameters_of_service"] - ): - # assert isinstance(service_dict["diag_layer"], str) + def print_dl_changes(self, service_spec: ServiceSpecs) -> None: + + if (service_spec["new_services"] or service_spec["deleted_services"] or + service_spec["changed_name_of_service"] or + service_spec["changed_parameters_of_service"]): rich_print() rich_print( - f"Changed diagnostic services for diagnostic layer '{service_dict['diag_layer']}' ({service_dict['diag_layer_type']}):" + f"Changed diagnostic services for diagnostic layer '{service_spec['diag_layer']}' ({service_spec['diag_layer_type']}):" ) - if service_dict["new_services"]: - # assert isinstance(service_dict["new_services"], List) + if service_spec["new_services"]: rich_print() rich_print(" [blue]New services[/blue]") - rich_print(extract_service_tabulation_data(service_dict["new_services"])) + rich_print(extract_service_tabulation_data(service_spec.new_services)) - if service_dict["deleted_services"]: - # assert isinstance(service_dict["deleted_services"], List) + if service_spec["deleted_services"]: rich_print() rich_print(" [blue]Deleted services[/blue]") - rich_print( - extract_service_tabulation_data(service_dict["deleted_services"]) - ) - if service_dict["changed_name_of_service"][0]: + rich_print(extract_service_tabulation_data(service_spec["deleted_services"])) + if service_spec["changed_name_of_service"][0]: rich_print() rich_print(" [blue]Renamed services[/blue]") - rich_print( - extract_service_tabulation_data( - service_dict["changed_name_of_service"][0] - ) - ) - if service_dict["changed_parameters_of_service"][0]: + rich_print(extract_service_tabulation_data(service_spec["changed_name_of_service"][0])) + + if service_spec["changed_parameters_of_service"][0]: rich_print() rich_print(" [blue]Services with parameter changes[/blue]") # create table with information about services with parameter changes changed_param_column = [ - str(x) for x in service_dict["changed_parameters_of_service"][1] + str(x) for x in service_spec["changed_parameters_of_service"][1] ] table = extract_service_tabulation_data( - service_dict["changed_parameters_of_service"][0], + service_spec["changed_parameters_of_service"][0], additional_columns=[("Changed Parameters", changed_param_column)], ) rich_print(table) - for service_idx, service in enumerate( - service_dict["changed_parameters_of_service"][0] - ): + for service_idx, service in enumerate(service_spec["changed_parameters_of_service"][0]): assert isinstance(service, DiagService) rich_print() rich_print( f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" ) # detailed_info in [infotext1, dict1, infotext2, dict2, ...] - info_list = cast(list, service_dict["changed_parameters_of_service"])[ - 2 - ][service_idx] + info_list = cast( + list, # type: ignore[type-arg] + service_spec["changed_parameters_of_service"])[2][service_idx] + for detailed_info in info_list: if isinstance(detailed_info, str): rich_print() @@ -139,24 +128,20 @@ def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None # prints result of database comparison (input variable: dictionary: changes_variants) # diagnostic layers - if ( - changes_variants["new_diagnostic_layers"] - or changes_variants["deleted_diagnostic_layers"] - ): + if (changes_variants["new_diagnostic_layers"] or + changes_variants["deleted_diagnostic_layers"]): rich_print() rich_print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") rich_print(" New diagnostic layers: ") for variant in changes_variants["new_diagnostic_layers"]: assert isinstance(variant, DiagLayer) rich_print( - f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})" - ) + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") rich_print(" Deleted diagnostic layers: ") for variant in changes_variants["deleted_diagnostic_layers"]: assert isinstance(variant, DiagLayer) rich_print( - f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})" - ) + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") # diagnostic services for _, value in changes_variants.items(): @@ -175,9 +160,7 @@ class Comparison(Display): def __init__(self) -> None: pass - def compare_parameters( - self, param1: Parameter, param2: Parameter - ) -> Dict[str, Any]: + def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, Any]: # checks whether properties of param1 and param2 differ # checked properties: Name, Byte Position, Bit Length, Semantic, Parameter Type, Value (Coded, Constant, Default etc.), Data Type, Data Object Property (Name, Physical Data Type, Unit) @@ -209,52 +192,35 @@ def append_list( if param1.parameter_type != param2.parameter_type: append_list("Parameter type", param1.parameter_type, param2.parameter_type) - if isinstance(param1, CodedConstParameter) and isinstance( - param2, CodedConstParameter - ): - if ( - param1.diag_coded_type.base_data_type - != param2.diag_coded_type.base_data_type - ): + if isinstance(param1, CodedConstParameter) and isinstance(param2, CodedConstParameter): + if (param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type): append_list( "Data type", param1.diag_coded_type.base_data_type.name, param2.diag_coded_type.base_data_type.name, ) if param1.coded_value != param2.coded_value: - if isinstance(param1.coded_value, int) and isinstance( - param2.coded_value, int - ): + if isinstance(param1.coded_value, int) and isinstance(param2.coded_value, int): append_list( "Value", f"0x{param1.coded_value:0{(param1.get_static_bit_length() or 0) // 4}X}", f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}", ) else: - append_list( - "Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}" - ) + append_list("Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}") - elif isinstance(param1, NrcConstParameter) and isinstance( - param2, NrcConstParameter - ): - if ( - param1.diag_coded_type.base_data_type - != param2.diag_coded_type.base_data_type - ): + elif isinstance(param1, NrcConstParameter) and isinstance(param2, NrcConstParameter): + if (param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type): append_list( "Data type", param1.diag_coded_type.base_data_type.name, param2.diag_coded_type.base_data_type.name, ) if param1.coded_values != param2.coded_values: - append_list( - "Values", str(param1.coded_values), str(param2.coded_values) - ) + append_list("Values", str(param1.coded_values), str(param2.coded_values)) - elif (dop_1 := getattr(param1, "dop", None)) is not None and ( - dop_2 := getattr(param2, "dop", None) - ) is not None: + elif (dop_1 := getattr(param1, "dop", None)) is not None and (dop_2 := getattr( + param2, "dop", None)) is not None: if dop_1 != dop_2: # TODO: compare INTERNAL-CONSTR, COMPU-INTERNAL-TO-PHYS of DOP @@ -267,19 +233,15 @@ def append_list( # DOP Unit if getattr(dop_1, "unit", None) and getattr(dop_2, "unit", None): # (properties of unit object: short_name, long_name, description, odx_id, display_name, oid, factor_si_to_unit, offset_si_to_unit, physical_dimension_ref) - if ( - dop_1.unit != dop_2.unit - and dop_1.unit.short_name != dop_2.unit.short_name - ): + if (dop_1.unit != dop_2.unit and + dop_1.unit.short_name != dop_2.unit.short_name): append_list( " DOP unit name", dop_1.unit.short_name, dop_2.unit.short_name, ) - elif ( - dop_1.unit != dop_2.unit - and dop_1.unit.display_name != dop_2.unit.display_name - ): + elif (dop_1.unit != dop_2.unit and + dop_1.unit.display_name != dop_2.unit.display_name): append_list( " DOP unit display name", dop_1.unit.display_name, @@ -289,26 +251,20 @@ def append_list( append_list(" DOP unit object", "", "") if hasattr(dop_1, "physical_type") and hasattr(dop_2, "physical_type"): - if ( - dop_1.physical_type - and dop_2.physical_type - and dop_1.physical_type.base_data_type - != dop_2.physical_type.base_data_type - ): + if (dop_1.physical_type and dop_2.physical_type and + dop_1.physical_type.base_data_type + != dop_2.physical_type.base_data_type): append_list( " DOP physical data type", dop_1.physical_type.base_data_type.name, dop_2.physical_type.base_data_type.name, ) - if ( - isinstance(param1, PhysicalConstantParameter) - and isinstance(param2, PhysicalConstantParameter) - and param1.physical_constant_value != param2.physical_constant_value - ): + if (isinstance(param1, PhysicalConstantParameter) and + isinstance(param2, PhysicalConstantParameter) and + param1.physical_constant_value != param2.physical_constant_value): if isinstance(param1.physical_constant_value, int) and isinstance( - param2.physical_constant_value, int - ): + param2.physical_constant_value, int): append_list( "Constant value", f"0x{param1.physical_constant_value:0{(param1.get_static_bit_length() or 0) // 4}X}", @@ -321,16 +277,12 @@ def append_list( f"{param2.physical_constant_value!r}", ) - elif ( - isinstance(param1, ValueParameter) - and isinstance(param2, ValueParameter) - and param1.physical_default_value is not None - and param2.physical_default_value is not None - and param1.physical_default_value != param2.physical_default_value - ): + elif (isinstance(param1, ValueParameter) and isinstance(param2, ValueParameter) and + param1.physical_default_value is not None and + param2.physical_default_value is not None and + param1.physical_default_value != param2.physical_default_value): if isinstance(param1.physical_default_value, int) and isinstance( - param2.physical_default_value, int - ): + param2.physical_default_value, int): append_list( "Default value", f"0x{param1.physical_default_value:0{(param1.get_static_bit_length() or 0) // 4}X}", @@ -345,62 +297,44 @@ def append_list( return {"Property": property, "Old Value": old, "New Value": new} - def compare_services( - self, service1: DiagService, service2: DiagService - ) -> List[SpecsServiceDict]: + def compare_services(self, service1: DiagService, service2: DiagService) -> List[ServiceSpecs]: # compares request, positive response and negative response parameters of two diagnostic services information: List[Union[str, Dict[str, Any]]] = ( - [] - ) # information = [infotext1, table1, infotext2, table2, ...] + []) # information = [infotext1, table1, infotext2, table2, ...] changed_params: str = "" # Request - if ( - service1.request is not None - and service2.request is not None - and len(service1.request.parameters) == len(service2.request.parameters) - ): + if (service1.request is not None and service2.request is not None and + len(service1.request.parameters) == len(service2.request.parameters)): for res1_idx, param1 in enumerate(service1.request.parameters): for res2_idx, param2 in enumerate(service2.request.parameters): if res1_idx == res2_idx: # find changed request parameter properties table = self.compare_parameters(param1, param2) - infotext = ( - f" Properties of request parameter '{param2.short_name}' " - f"that have changed:\n" - ) + infotext = (f" Properties of request parameter '{param2.short_name}' " + f"that have changed:\n") # array index starts with 0 -> param[0] is 1. service parameter if table["Property"]: information.append(infotext) information.append(table) - changed_params += ( - f"request parameter '{param2.short_name}',\n" - ) + changed_params += (f"request parameter '{param2.short_name}',\n") else: changed_params += "request parameter list, " # infotext - information.append( - f"List of request parameters for service '{service2.short_name}' " - f"is not identical.\n" - ) + information.append(f"List of request parameters for service '{service2.short_name}' " + f"is not identical.\n") # table - param_list1 = ( - [] if service1.request is None else service1.request.parameters - ) - param_list2 = ( - [] if service2.request is None else service2.request.parameters - ) + param_list1 = ([] if service1.request is None else service1.request.parameters) + param_list2 = ([] if service2.request is None else service2.request.parameters) - information.append( - { - "List": ["Old list", "New list"], - "Values": [f"\\{param_list1}", f"\\{param_list2}"], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [f"\\{param_list1}", f"\\{param_list2}"], + }) # Positive Responses if len(service1.positive_responses) == len(service2.positive_responses): @@ -409,16 +343,13 @@ def compare_services( if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate( - response2.parameters - ): + for param2_idx, param2 in enumerate(response2.parameters): if param1_idx == param2_idx: # find changed positive response parameter properties table = self.compare_parameters(param1, param2) infotext = ( f" Properties of positive response parameter '{param2.short_name}' that " - f"have changed:\n" - ) + f"have changed:\n") # array index starts with 0 -> param[0] is first service parameter if table["Property"]: @@ -432,31 +363,26 @@ def compare_services( f"List of positive response parameters for service '{service2.short_name}' is not identical." ) # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [ + str(response1.parameters), + str(response2.parameters), + ], + }) else: changed_params += "positive responses list, " # infotext information.append( - f"List of positive responses for service '{service2.short_name}' is not identical." - ) + f"List of positive responses for service '{service2.short_name}' is not identical.") # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(service1.positive_responses), - str(service2.positive_responses), - ], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [ + str(service1.positive_responses), + str(service2.positive_responses), + ], + }) # Negative Responses if len(service1.negative_responses) == len(service2.negative_responses): @@ -465,9 +391,7 @@ def compare_services( if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate( - response2.parameters - ): + for param2_idx, param2 in enumerate(response2.parameters): if param1_idx == param2_idx: # find changed negative response parameter properties table = self.compare_parameters(param1, param2) @@ -485,15 +409,13 @@ def compare_services( f"List of positive response parameters for service '{service2.short_name}' is not identical.\n" ) # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [ + str(response1.parameters), + str(response2.parameters), + ], + }) else: changed_params += "negative responses list, " # infotext @@ -501,26 +423,22 @@ def compare_services( f"List of positive responses for service '{service2.short_name}' is not identical.\n" ) # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(service1.negative_responses), - str(service2.negative_responses), - ], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [ + str(service1.negative_responses), + str(service2.negative_responses), + ], + }) return [information, changed_params] - def compare_diagnostic_layers( - self, dl1: DiagLayer, dl2: DiagLayer - ) -> SpecsServiceDict: + def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSpecs: # compares diagnostic services of two diagnostic layers with each other # save changes in dictionary (service_dict) # TODO: add comparison of SingleECUJobs - service_dict: SpecsServiceDict = { + service_dict: ServiceSpecs = { "diag_layer": dl1.short_name, "diag_layer_type": dl1.variant_type.value, # list with added diagnostic services [service1, service2, service3, ...] Type: DiagService @@ -540,12 +458,10 @@ def compare_diagnostic_layers( # extract the constant prefixes for the requests of all # services (used for duck-typed rename detection) dl1_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() - for s in dl1.services + None if s.request is None else s.request.coded_const_prefix() for s in dl1.services ] dl2_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() - for s in dl2.services + None if s.request is None else s.request.coded_const_prefix() for s in dl2.services ] # compare diagnostic services @@ -560,9 +476,7 @@ def compare_diagnostic_layers( if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - service_dict["new_services"].append( - service1 - ) # type: ignore[arg-type] + service_dict["new_services"].append(service1) # type: ignore[arg-type] # check whether names of diagnostic services have changed elif service1 not in dl2.services: @@ -576,45 +490,34 @@ def compare_diagnostic_layers( # add new service (type: DiagService) service_dict["changed_name_of_service"][0].append(service1) # add old service name (type: String) - service_dict["changed_name_of_service"][1].append( - service2.short_name - ) + service_dict["changed_name_of_service"][1].append(service2.short_name) # compare request, pos. response and neg. response parameters of diagnostic services detailed_information = self.compare_services(service1, service2) # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[ - 1 - ]: # check whether string "changed_params" is empty + if detailed_information[1]: # check whether string "changed_params" is empty # new service (type: DiagService) - service_dict["changed_parameters_of_service"][0].append( - service1 - ) + service_dict["changed_parameters_of_service"][0].append(service1) # add parameters which have been changed (type: String) service_dict["changed_parameters_of_service"][1].append( - detailed_information[1] - ) + detailed_information[1]) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] service_dict["changed_parameters_of_service"][2].append( - detailed_information[0] - ) + detailed_information[0]) for service2_idx, service2 in enumerate(dl2.services): # check for deleted diagnostic services - if ( - service2.short_name not in dl1_service_names - and dl2_request_prefixes[service2_idx] not in dl1_request_prefixes - ): + if (service2.short_name not in dl1_service_names and + dl2_request_prefixes[service2_idx] not in dl1_request_prefixes): deleted_list = service_dict["deleted_services"] assert isinstance(deleted_list, list) if service2 not in deleted_list: service_dict["deleted_services"].append( # type: ignore[union-attr] - service2 - ) + service2) if service1.short_name == service2.short_name: # compare request, pos. response and neg. response parameters of both diagnostic services @@ -622,28 +525,19 @@ def compare_diagnostic_layers( # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[ - 1 - ]: # check whether string "changed_params" is empty + if detailed_information[1]: # check whether string "changed_params" is empty # new service (type: DiagService) - service_dict["changed_parameters_of_service"][0].append( - service1 - ) + service_dict["changed_parameters_of_service"][0].append(service1) # add parameters which have been changed (type: String) service_dict["changed_parameters_of_service"][1].append( - detailed_information[1] - ) + detailed_information[1]) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] service_dict["changed_parameters_of_service"][ # type: ignore[union-attr] - 2 - ].append( - detailed_information[0] - ) # type: ignore[arg-type] + 2].append(detailed_information[0]) # type: ignore[arg-type] return service_dict - def compare_databases( - self, database_new: Database, database_old: Database - ) -> SpecsChangesVariants: # type: ignore[type-arg] + def compare_databases(self, database_new: Database, + database_old: Database) -> SpecsChangesVariants: # type: ignore[type-arg] # compares two PDX-files with each other new_variants = [] @@ -662,27 +556,18 @@ def compare_databases( for _, dl2 in enumerate(database_old.diag_layers): # check for deleted diagnostic layers - if ( - dl2.short_name - not in [dl.short_name for dl in database_new.diag_layers] - and dl2 not in changes_variants["deleted_diagnostic_layers"] - ): + if (dl2.short_name not in [dl.short_name for dl in database_new.diag_layers] and + dl2 not in changes_variants["deleted_diagnostic_layers"]): changes_variants[ - "deleted_diagnostic_layers" - ].append( # type: ignore[union-attr] - dl2 - ) + "deleted_diagnostic_layers"].append( # type: ignore[union-attr] + dl2) - if ( - dl1.short_name == dl2.short_name - and dl1.short_name in self.diagnostic_layer_names - ): + if (dl1.short_name == dl2.short_name and + dl1.short_name in self.diagnostic_layer_names): # compare diagnostic services of both diagnostic layers # save diagnostic service changes in dictionary (empty if no changes) - service_dict: SpecsServiceDict = self.compare_diagnostic_layers( - dl1, dl2 - ) + service_dict: ServiceSpecs = self.compare_diagnostic_layers(dl1, dl2) if service_dict: # adds information about diagnostic service changes to return variable (changes_variants) changes_variants.update({dl1.short_name: service_dict}) @@ -693,19 +578,17 @@ def compare_databases( def add_subparser(subparsers: SubparsersList) -> None: parser = subparsers.add_parser( "compare", - description="\n".join( - [ - "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", - "", - "Examples:", - " Comparison of two diagnostic layers:", - " odxtools compare ./path/to/database.pdx -v variant1 variant2", - " Comparison of two database versions:", - " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", - " For more information use:", - " odxtools compare -h", - ] - ), + description="\n".join([ + "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", + "", + "Examples:", + " Comparison of two diagnostic layers:", + " odxtools compare ./path/to/database.pdx -v variant1 variant2", + " Comparison of two database versions:", + " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", + " For more information use:", + " odxtools compare -h", + ]), help="Compares two versions of diagnostic layers and/or databases with each other. Checks whether diagnostic services and its parameters have changed.", formatter_class=argparse.RawTextHelpFormatter, ) @@ -752,9 +635,7 @@ def run(args: argparse.Namespace) -> None: task = Comparison() task.param_detailed = args.no_details - db_names = [ - args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0]) - ] + db_names = [args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0])] if args.database and args.variants: # compare specified databases, consider only specified variants @@ -763,9 +644,7 @@ def run(args: argparse.Namespace) -> None: db_names.append(name) if isinstance(name, str) else str(name[0]) task.databases = [load_file(name) for name in db_names] - diag_layer_names = { - dl.short_name for db in task.databases for dl in db.diag_layers - } + diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) @@ -786,32 +665,22 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" - ) - print_dl_metrics( - [ - variant - for variant in task.databases[0].diag_layers - if variant.short_name in task.diagnostic_layer_names - ] - ) + rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") + print_dl_metrics([ + variant for variant in task.databases[0].diag_layers + if variant.short_name in task.diagnostic_layer_names + ]) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})" - ) - print_dl_metrics( - [ - variant - for variant in task.databases[db_idx + 1].diag_layers - if variant.short_name in task.diagnostic_layer_names - ] - ) + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") + print_dl_metrics([ + variant for variant in task.databases[db_idx + 1].diag_layers + if variant.short_name in task.diagnostic_layer_names + ]) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1]) - ) + task.compare_databases(task.databases[0], task.databases[db_idx + 1])) elif args.database: # compare specified databases, consider all variants @@ -822,7 +691,9 @@ def run(args: argparse.Namespace) -> None: # collect all diagnostic layers from all specified databases task.diagnostic_layer_names = { - dl.short_name for db in task.databases for dl in db.diag_layers + dl.short_name + for db in task.databases + for dl in db.diag_layers } task.db_indicator_1 = 0 @@ -836,20 +707,16 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" - ) + rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") print_dl_metrics(list(task.databases[0].diag_layers)) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})" - ) + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") print_dl_metrics(list(task.databases[db_idx + 1].diag_layers)) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1]) - ) + task.compare_databases(task.databases[0], task.databases[db_idx + 1])) elif args.variants: # no databases specified -> comparison of diagnostic layers @@ -857,15 +724,11 @@ def run(args: argparse.Namespace) -> None: odxdb = _parser_utils.load_file(args) task.databases = [odxdb] - diag_layer_names = { - dl.short_name for db in task.databases for dl in db.diag_layers - } + diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) task.diagnostic_layers = [ - dl - for db in task.databases - for dl in db.diag_layers + dl for db in task.databases for dl in db.diag_layers if dl.short_name in task.diagnostic_layer_names ] @@ -883,15 +746,12 @@ def run(args: argparse.Namespace) -> None: break rich_print() - rich_print( - f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})" - ) + rich_print(f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})") rich_print( f" (compared to '{task.diagnostic_layers[db_idx + 1].short_name}' ({task.diagnostic_layers[db_idx + 1].variant_type.value}))" ) task.print_dl_changes( - task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1]) - ) + task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1])) else: # no databases & no variants specified From 22241df19ad29fddb28c98531a2af48d21e80515 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 23 Mar 2025 21:18:03 +0000 Subject: [PATCH 05/20] refractored the code by adding dataclass and formated using reformat.sh --- odxtools/cli/compare.py | 177 +++++++++++++++++----------------------- 1 file changed, 75 insertions(+), 102 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 80692169..ec95b30b 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -4,7 +4,7 @@ import argparse import os from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Set, Tuple, TypedDict, Union, cast +from typing import Any, Dict, List, Optional, Set, Tuple, Union from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -33,73 +33,77 @@ class ServiceSpecs: diag_layer: str diag_layer_type: str - new_services: List[str] # List of new service names + new_services: List[DiagService] # List of new service names deleted_services: List[DiagService] # List of deleted services - renamed_service: List[Tuple[str, DiagService]] # List of (old_name, DiagService) + # renamed_service: List[Tuple[str, DiagService]] # List of (old_name, DiagService) + + renamed_service: list # type: ignore[type-arg] + # List of (old_name, DiagService) changed_name_of_service: List[Tuple[str, DiagService]] # Similar to renamed_service - changed_parameters_of_service: List[Tuple[str, DiagService, List[Union[str, int, float]]]] + # changed_parameters_of_service: List[Tuple[DiagService, List[Union[str, int, float]]]] + # changed_parameters_of_service: List[Tuple[str, List[str]]] + changed_parameters_of_service: list[Any, Any, DiagService] # type: ignore[type-arg] @dataclass class SpecsChangesVariants: - new_variants: List[str] - deleted_variants: List[str] - service_changes: ServiceSpecs + new_diagnostic_layers: List[str] + deleted_diagnostic_layers: List[DiagLayer] + service_changes: Dict[str, ServiceSpecs] class Display: - param_detailed: bool = False - obj_detailed: bool = False + param_detailed: bool + obj_detailed: bool def __init__(self) -> None: pass def print_dl_changes(self, service_spec: ServiceSpecs) -> None: - - if (service_spec["new_services"] or service_spec["deleted_services"] or - service_spec["changed_name_of_service"] or - service_spec["changed_parameters_of_service"]): + if (service_spec.new_services or service_spec.deleted_services or + service_spec.changed_name_of_service or service_spec.changed_parameters_of_service): + assert isinstance(service_spec.diag_layer, str) rich_print() rich_print( - f"Changed diagnostic services for diagnostic layer '{service_spec['diag_layer']}' ({service_spec['diag_layer_type']}):" + f"Changed diagnostic services for diagnostic layer '{service_spec.diag_layer}' ({service_spec.diag_layer_type}):" ) - if service_spec["new_services"]: + if service_spec.new_services: + assert isinstance(service_spec.new_services, List) rich_print() rich_print(" [blue]New services[/blue]") rich_print(extract_service_tabulation_data(service_spec.new_services)) - if service_spec["deleted_services"]: + if service_spec.deleted_services: + assert isinstance(service_spec.deleted_services, List) rich_print() rich_print(" [blue]Deleted services[/blue]") - rich_print(extract_service_tabulation_data(service_spec["deleted_services"])) - if service_spec["changed_name_of_service"][0]: + rich_print(extract_service_tabulation_data(service_spec.deleted_services)) + if service_spec.changed_name_of_service: + # assert isinstance(service_spec.changed_name_of_service[0], List) rich_print() rich_print(" [blue]Renamed services[/blue]") - rich_print(extract_service_tabulation_data(service_spec["changed_name_of_service"][0])) + renamed_services_objects = [service for _, service in service_spec.renamed_service] + rich_print(extract_service_tabulation_data(renamed_services_objects)) - if service_spec["changed_parameters_of_service"][0]: + if service_spec.changed_parameters_of_service: rich_print() rich_print(" [blue]Services with parameter changes[/blue]") # create table with information about services with parameter changes - changed_param_column = [ - str(x) for x in service_spec["changed_parameters_of_service"][1] - ] + changed_param_column = [str(x) for x in service_spec.changed_parameters_of_service] table = extract_service_tabulation_data( - service_spec["changed_parameters_of_service"][0], + service_spec.changed_parameters_of_service, additional_columns=[("Changed Parameters", changed_param_column)], ) rich_print(table) - for service_idx, service in enumerate(service_spec["changed_parameters_of_service"][0]): + for service_idx, service in enumerate(service_spec.changed_parameters_of_service): assert isinstance(service, DiagService) rich_print() rich_print( f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" ) # detailed_info in [infotext1, dict1, infotext2, dict2, ...] - info_list = cast( - list, # type: ignore[type-arg] - service_spec["changed_parameters_of_service"])[2][service_idx] + info_list = service_spec.changed_parameters_of_service[2][service_idx] for detailed_info in info_list: if isinstance(detailed_info, str): @@ -128,25 +132,15 @@ def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None # prints result of database comparison (input variable: dictionary: changes_variants) # diagnostic layers - if (changes_variants["new_diagnostic_layers"] or - changes_variants["deleted_diagnostic_layers"]): + if (changes_variants.new_diagnostic_layers or changes_variants.deleted_diagnostic_layers): rich_print() rich_print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") rich_print(" New diagnostic layers: ") - for variant in changes_variants["new_diagnostic_layers"]: + for variant in changes_variants.new_diagnostic_layers: assert isinstance(variant, DiagLayer) rich_print( f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") rich_print(" Deleted diagnostic layers: ") - for variant in changes_variants["deleted_diagnostic_layers"]: - assert isinstance(variant, DiagLayer) - rich_print( - f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") - - # diagnostic services - for _, value in changes_variants.items(): - if isinstance(value, dict): - self.print_dl_changes(value) class Comparison(Display): @@ -221,7 +215,6 @@ def append_list( elif (dop_1 := getattr(param1, "dop", None)) is not None and (dop_2 := getattr( param2, "dop", None)) is not None: - if dop_1 != dop_2: # TODO: compare INTERNAL-CONSTR, COMPU-INTERNAL-TO-PHYS of DOP append_list("Linked DOP object", "", "") @@ -297,12 +290,13 @@ def append_list( return {"Property": property, "Old Value": old, "New Value": new} - def compare_services(self, service1: DiagService, service2: DiagService) -> List[ServiceSpecs]: + def compare_services(self, service1: DiagService, + service2: DiagService) -> List: # type: ignore[type-arg] # compares request, positive response and negative response parameters of two diagnostic services - information: List[Union[str, Dict[str, Any]]] = ( - []) # information = [infotext1, table1, infotext2, table2, ...] - changed_params: str = "" + information: List[Union[str, Dict[str, Any]]] = [ + ] # information = [infotext1, table1, infotext2, table2, ...] + changed_params = "" # Request if (service1.request is not None and service2.request is not None and @@ -434,24 +428,19 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List return [information, changed_params] def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSpecs: - # compares diagnostic services of two diagnostic layers with each other - # save changes in dictionary (service_dict) - # TODO: add comparison of SingleECUJobs - - service_dict: ServiceSpecs = { - "diag_layer": dl1.short_name, - "diag_layer_type": dl1.variant_type.value, + service_spec: ServiceSpecs = ServiceSpecs( + diag_layer=dl1.short_name, + diag_layer_type=dl1.variant_type.value, # list with added diagnostic services [service1, service2, service3, ...] Type: DiagService - "new_services": [], + new_services=[], # list with deleted diagnostic services [service1, service2, service3, ...] Type: DiagService - "deleted_services": [], + deleted_services=[], # list with diagnostic services where the service name changed [[services], [old service names]] - "changed_name_of_service": [[], []], + renamed_service=[], + changed_name_of_service=[], # list with diagnostic services where the service parameter changed [[services], [changed_parameters], [information_texts]] - "changed_parameters_of_service": [[], [], []], - } - # service_dict["changed_name_of_service"][{0 = services, 1 = old service names}][i] - # service_dict["changed_parameters_of_service"][{0 = services, 1 = changed_parameters, 2 = information_texts}][i] + changed_parameters_of_service=[], + ) dl1_service_names = [service.short_name for service in dl1.services] @@ -466,7 +455,6 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # compare diagnostic services for service1 in dl1.services: - # check for added diagnostic services rq_prefix: Optional[bytes] = None if service1.request is not None: @@ -476,7 +464,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - service_dict["new_services"].append(service1) # type: ignore[arg-type] + service_spec.new_services.append(service1) # check whether names of diagnostic services have changed elif service1 not in dl2.services: @@ -488,9 +476,9 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # save information about changes in dictionary # add new service (type: DiagService) - service_dict["changed_name_of_service"][0].append(service1) + service_spec.changed_name_of_service.append((service2.short_name, service1)) # add old service name (type: String) - service_dict["changed_name_of_service"][1].append(service2.short_name) + # service_spec.changed_name_of_service[1].append(service2.short_name) # compare request, pos. response and neg. response parameters of diagnostic services detailed_information = self.compare_services(service1, service2) @@ -498,26 +486,17 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # add information about changed diagnostic service parameters to dicitionary if detailed_information[1]: # check whether string "changed_params" is empty - # new service (type: DiagService) - service_dict["changed_parameters_of_service"][0].append(service1) - # add parameters which have been changed (type: String) - service_dict["changed_parameters_of_service"][1].append( - detailed_information[1]) - # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] - service_dict["changed_parameters_of_service"][2].append( - detailed_information[0]) + service_spec.changed_parameters_of_service.append( + (detailed_information[1], detailed_information[0], service1)) for service2_idx, service2 in enumerate(dl2.services): - # check for deleted diagnostic services if (service2.short_name not in dl1_service_names and dl2_request_prefixes[service2_idx] not in dl1_request_prefixes): - - deleted_list = service_dict["deleted_services"] + deleted_list = service_spec.deleted_services assert isinstance(deleted_list, list) if service2 not in deleted_list: - service_dict["deleted_services"].append( # type: ignore[union-attr] - service2) + service_spec.deleted_services.append(service2) if service1.short_name == service2.short_name: # compare request, pos. response and neg. response parameters of both diagnostic services @@ -527,50 +506,45 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # add information about changed diagnostic service parameters to dicitionary if detailed_information[1]: # check whether string "changed_params" is empty # new service (type: DiagService) - service_dict["changed_parameters_of_service"][0].append(service1) + service_spec.changed_parameters_of_service.append(service1) # add parameters which have been changed (type: String) - service_dict["changed_parameters_of_service"][1].append( - detailed_information[1]) + service_spec.changed_parameters_of_service.append(detailed_information[1]) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] - service_dict["changed_parameters_of_service"][ # type: ignore[union-attr] - 2].append(detailed_information[0]) # type: ignore[arg-type] - return service_dict + service_spec.changed_parameters_of_service.append(detailed_information[0]) + return service_spec def compare_databases(self, database_new: Database, - database_old: Database) -> SpecsChangesVariants: # type: ignore[type-arg] + database_old: Database) -> SpecsChangesVariants: # compares two PDX-files with each other - new_variants = [] - deleted_variants = [] + deleted_variants: list[DiagLayer] = [] - changes_variants: SpecsChangesVariants = { - "new_diagnostic_layers": new_variants, - "deleted_diagnostic_layers": deleted_variants, - } + # Create the SpecsChangesVariants instance + changes_variants: SpecsChangesVariants = SpecsChangesVariants( + new_diagnostic_layers=[], + deleted_diagnostic_layers=deleted_variants, + service_changes={}, # You can populate this dictionary later as needed + ) # compare databases for _, dl1 in enumerate(database_new.diag_layers): # check for new diagnostic layers if dl1.short_name not in [dl.short_name for dl in database_old.diag_layers]: - changes_variants["new_diagnostic_layers"].append(dl1) # type: ignore[union-attr] + changes_variants.new_diagnostic_layers.append(dl1.short_name) for _, dl2 in enumerate(database_old.diag_layers): # check for deleted diagnostic layers if (dl2.short_name not in [dl.short_name for dl in database_new.diag_layers] and - dl2 not in changes_variants["deleted_diagnostic_layers"]): - - changes_variants[ - "deleted_diagnostic_layers"].append( # type: ignore[union-attr] - dl2) - + dl2 not in changes_variants.deleted_diagnostic_layers): + changes_variants.deleted_diagnostic_layers.append(dl2) if (dl1.short_name == dl2.short_name and dl1.short_name in self.diagnostic_layer_names): # compare diagnostic services of both diagnostic layers # save diagnostic service changes in dictionary (empty if no changes) - service_dict: ServiceSpecs = self.compare_diagnostic_layers(dl1, dl2) - if service_dict: + service_spec: ServiceSpecs = self.compare_diagnostic_layers(dl1, dl2) + if service_spec: # adds information about diagnostic service changes to return variable (changes_variants) - changes_variants.update({dl1.short_name: service_dict}) + changes_variants.service_changes[dl1.short_name] = service_spec return changes_variants @@ -631,7 +605,6 @@ def add_subparser(subparsers: SubparsersList) -> None: def run(args: argparse.Namespace) -> None: - task = Comparison() task.param_detailed = args.no_details @@ -673,7 +646,7 @@ def run(args: argparse.Namespace) -> None: rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})") print_dl_metrics([ variant for variant in task.databases[db_idx + 1].diag_layers if variant.short_name in task.diagnostic_layer_names @@ -712,7 +685,7 @@ def run(args: argparse.Namespace) -> None: rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})") print_dl_metrics(list(task.databases[db_idx + 1].diag_layers)) task.print_database_changes( From c6e45d7504a055f38afd0457edd91c61be33a832 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 23 Mar 2025 21:26:05 +0000 Subject: [PATCH 06/20] refractored the code by adding dataclass and formated using reformat.sh file --- odxtools/cli/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 odxtools/cli/.gitignore diff --git a/odxtools/cli/.gitignore b/odxtools/cli/.gitignore new file mode 100644 index 00000000..78141cd9 --- /dev/null +++ b/odxtools/cli/.gitignore @@ -0,0 +1 @@ +debug_log.txt From cf1288b923214bb1c8d976c511999a26ad2e2a95 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 23 Mar 2025 21:35:41 +0000 Subject: [PATCH 07/20] refractored the code by adding dataclass and formated using reformat.sh file --- odxtools/cli/compare.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index ec95b30b..18b30c3e 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -35,13 +35,8 @@ class ServiceSpecs: diag_layer_type: str new_services: List[DiagService] # List of new service names deleted_services: List[DiagService] # List of deleted services - # renamed_service: List[Tuple[str, DiagService]] # List of (old_name, DiagService) - renamed_service: list # type: ignore[type-arg] - # List of (old_name, DiagService) - changed_name_of_service: List[Tuple[str, DiagService]] # Similar to renamed_service - # changed_parameters_of_service: List[Tuple[DiagService, List[Union[str, int, float]]]] - # changed_parameters_of_service: List[Tuple[str, List[str]]] + changed_name_of_service: List[Tuple[str, DiagService]] changed_parameters_of_service: list[Any, Any, DiagService] # type: ignore[type-arg] From c32a2cf414fe3bc19217cde3108c7cfe39f4f55b Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 23 Mar 2025 21:55:14 +0000 Subject: [PATCH 08/20] refractored the code by adding dataclass and formated using reformat.sh fil --- odxtools/cli/compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 18b30c3e..fc46092b 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -33,7 +33,7 @@ class ServiceSpecs: diag_layer: str diag_layer_type: str - new_services: List[DiagService] # List of new service names + new_services: List[DiagService] # List of new service name deleted_services: List[DiagService] # List of deleted services renamed_service: list # type: ignore[type-arg] changed_name_of_service: List[Tuple[str, DiagService]] From 4d35bd9888bfa406bf75d60794899addad58d2f4 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 23 Mar 2025 21:57:27 +0000 Subject: [PATCH 09/20] refractored the code by adding dataclass and formated using reformat.sh --- odxtools/cli/compare.py | 447 ++++++++++++++++++++++++++-------------- 1 file changed, 287 insertions(+), 160 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index fc46092b..00d94bdd 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -22,8 +22,11 @@ from ..parameters.valueparameter import ValueParameter from . import _parser_utils from ._parser_utils import SubparsersList -from ._print_utils import (extract_service_tabulation_data, print_dl_metrics, - print_service_parameters) +from ._print_utils import ( + extract_service_tabulation_data, + print_dl_metrics, + print_service_parameters, +) # name of the tool _odxtools_tool_name_ = "compare" @@ -36,7 +39,7 @@ class ServiceSpecs: new_services: List[DiagService] # List of new service name deleted_services: List[DiagService] # List of deleted services renamed_service: list # type: ignore[type-arg] - changed_name_of_service: List[Tuple[str, DiagService]] + changed_name_of_service: List[Tuple[str, DiagService]] changed_parameters_of_service: list[Any, Any, DiagService] # type: ignore[type-arg] @@ -54,51 +57,61 @@ class Display: def __init__(self) -> None: pass - def print_dl_changes(self, service_spec: ServiceSpecs) -> None: - if (service_spec.new_services or service_spec.deleted_services or - service_spec.changed_name_of_service or service_spec.changed_parameters_of_service): - assert isinstance(service_spec.diag_layer, str) + def print_dl_changes(self, service_specs: ServiceSpecs) -> None: + if ( + service_specs.new_services + or service_specs.deleted_services + or service_specs.changed_name_of_service + or service_specs.changed_parameters_of_service + ): + assert isinstance(service_specs.diag_layer, str) rich_print() rich_print( - f"Changed diagnostic services for diagnostic layer '{service_spec.diag_layer}' ({service_spec.diag_layer_type}):" + f"Changed diagnostic services for diagnostic layer '{service_specs.diag_layer}' ({service_specs.diag_layer_type}):" ) - if service_spec.new_services: - assert isinstance(service_spec.new_services, List) + if service_specs.new_services: + assert isinstance(service_specs.new_services, List) rich_print() rich_print(" [blue]New services[/blue]") - rich_print(extract_service_tabulation_data(service_spec.new_services)) + rich_print(extract_service_tabulation_data(service_specs.new_services)) - if service_spec.deleted_services: - assert isinstance(service_spec.deleted_services, List) + if service_specs.deleted_services: + assert isinstance(service_specs.deleted_services, List) rich_print() rich_print(" [blue]Deleted services[/blue]") - rich_print(extract_service_tabulation_data(service_spec.deleted_services)) - if service_spec.changed_name_of_service: - # assert isinstance(service_spec.changed_name_of_service[0], List) + rich_print(extract_service_tabulation_data(service_specs.deleted_services)) + if service_specs.changed_name_of_service: + # assert isinstance(service_specs.changed_name_of_service[0], List) rich_print() rich_print(" [blue]Renamed services[/blue]") - renamed_services_objects = [service for _, service in service_spec.renamed_service] + renamed_services_objects = [ + service for _, service in service_specs.renamed_service + ] rich_print(extract_service_tabulation_data(renamed_services_objects)) - if service_spec.changed_parameters_of_service: + if service_specs.changed_parameters_of_service: rich_print() rich_print(" [blue]Services with parameter changes[/blue]") # create table with information about services with parameter changes - changed_param_column = [str(x) for x in service_spec.changed_parameters_of_service] + changed_param_column = [ + str(x) for x in service_specs.changed_parameters_of_service + ] table = extract_service_tabulation_data( - service_spec.changed_parameters_of_service, + service_specs.changed_parameters_of_service, additional_columns=[("Changed Parameters", changed_param_column)], ) rich_print(table) - for service_idx, service in enumerate(service_spec.changed_parameters_of_service): + for service_idx, service in enumerate( + service_specs.changed_parameters_of_service + ): assert isinstance(service, DiagService) rich_print() rich_print( f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" ) # detailed_info in [infotext1, dict1, infotext2, dict2, ...] - info_list = service_spec.changed_parameters_of_service[2][service_idx] + info_list = service_specs.changed_parameters_of_service[2][service_idx] for detailed_info in info_list: if isinstance(detailed_info, str): @@ -127,14 +140,18 @@ def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None # prints result of database comparison (input variable: dictionary: changes_variants) # diagnostic layers - if (changes_variants.new_diagnostic_layers or changes_variants.deleted_diagnostic_layers): + if ( + changes_variants.new_diagnostic_layers + or changes_variants.deleted_diagnostic_layers + ): rich_print() rich_print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") rich_print(" New diagnostic layers: ") for variant in changes_variants.new_diagnostic_layers: assert isinstance(variant, DiagLayer) rich_print( - f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})" + ) rich_print(" Deleted diagnostic layers: ") @@ -149,7 +166,9 @@ class Comparison(Display): def __init__(self) -> None: pass - def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, Any]: + def compare_parameters( + self, param1: Parameter, param2: Parameter + ) -> Dict[str, Any]: # checks whether properties of param1 and param2 differ # checked properties: Name, Byte Position, Bit Length, Semantic, Parameter Type, Value (Coded, Constant, Default etc.), Data Type, Data Object Property (Name, Physical Data Type, Unit) @@ -181,35 +200,52 @@ def append_list( if param1.parameter_type != param2.parameter_type: append_list("Parameter type", param1.parameter_type, param2.parameter_type) - if isinstance(param1, CodedConstParameter) and isinstance(param2, CodedConstParameter): - if (param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type): + if isinstance(param1, CodedConstParameter) and isinstance( + param2, CodedConstParameter + ): + if ( + param1.diag_coded_type.base_data_type + != param2.diag_coded_type.base_data_type + ): append_list( "Data type", param1.diag_coded_type.base_data_type.name, param2.diag_coded_type.base_data_type.name, ) if param1.coded_value != param2.coded_value: - if isinstance(param1.coded_value, int) and isinstance(param2.coded_value, int): + if isinstance(param1.coded_value, int) and isinstance( + param2.coded_value, int + ): append_list( "Value", f"0x{param1.coded_value:0{(param1.get_static_bit_length() or 0) // 4}X}", f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}", ) else: - append_list("Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}") + append_list( + "Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}" + ) - elif isinstance(param1, NrcConstParameter) and isinstance(param2, NrcConstParameter): - if (param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type): + elif isinstance(param1, NrcConstParameter) and isinstance( + param2, NrcConstParameter + ): + if ( + param1.diag_coded_type.base_data_type + != param2.diag_coded_type.base_data_type + ): append_list( "Data type", param1.diag_coded_type.base_data_type.name, param2.diag_coded_type.base_data_type.name, ) if param1.coded_values != param2.coded_values: - append_list("Values", str(param1.coded_values), str(param2.coded_values)) + append_list( + "Values", str(param1.coded_values), str(param2.coded_values) + ) - elif (dop_1 := getattr(param1, "dop", None)) is not None and (dop_2 := getattr( - param2, "dop", None)) is not None: + elif (dop_1 := getattr(param1, "dop", None)) is not None and ( + dop_2 := getattr(param2, "dop", None) + ) is not None: if dop_1 != dop_2: # TODO: compare INTERNAL-CONSTR, COMPU-INTERNAL-TO-PHYS of DOP append_list("Linked DOP object", "", "") @@ -221,15 +257,19 @@ def append_list( # DOP Unit if getattr(dop_1, "unit", None) and getattr(dop_2, "unit", None): # (properties of unit object: short_name, long_name, description, odx_id, display_name, oid, factor_si_to_unit, offset_si_to_unit, physical_dimension_ref) - if (dop_1.unit != dop_2.unit and - dop_1.unit.short_name != dop_2.unit.short_name): + if ( + dop_1.unit != dop_2.unit + and dop_1.unit.short_name != dop_2.unit.short_name + ): append_list( " DOP unit name", dop_1.unit.short_name, dop_2.unit.short_name, ) - elif (dop_1.unit != dop_2.unit and - dop_1.unit.display_name != dop_2.unit.display_name): + elif ( + dop_1.unit != dop_2.unit + and dop_1.unit.display_name != dop_2.unit.display_name + ): append_list( " DOP unit display name", dop_1.unit.display_name, @@ -239,20 +279,26 @@ def append_list( append_list(" DOP unit object", "", "") if hasattr(dop_1, "physical_type") and hasattr(dop_2, "physical_type"): - if (dop_1.physical_type and dop_2.physical_type and - dop_1.physical_type.base_data_type - != dop_2.physical_type.base_data_type): + if ( + dop_1.physical_type + and dop_2.physical_type + and dop_1.physical_type.base_data_type + != dop_2.physical_type.base_data_type + ): append_list( " DOP physical data type", dop_1.physical_type.base_data_type.name, dop_2.physical_type.base_data_type.name, ) - if (isinstance(param1, PhysicalConstantParameter) and - isinstance(param2, PhysicalConstantParameter) and - param1.physical_constant_value != param2.physical_constant_value): + if ( + isinstance(param1, PhysicalConstantParameter) + and isinstance(param2, PhysicalConstantParameter) + and param1.physical_constant_value != param2.physical_constant_value + ): if isinstance(param1.physical_constant_value, int) and isinstance( - param2.physical_constant_value, int): + param2.physical_constant_value, int + ): append_list( "Constant value", f"0x{param1.physical_constant_value:0{(param1.get_static_bit_length() or 0) // 4}X}", @@ -265,12 +311,16 @@ def append_list( f"{param2.physical_constant_value!r}", ) - elif (isinstance(param1, ValueParameter) and isinstance(param2, ValueParameter) and - param1.physical_default_value is not None and - param2.physical_default_value is not None and - param1.physical_default_value != param2.physical_default_value): + elif ( + isinstance(param1, ValueParameter) + and isinstance(param2, ValueParameter) + and param1.physical_default_value is not None + and param2.physical_default_value is not None + and param1.physical_default_value != param2.physical_default_value + ): if isinstance(param1.physical_default_value, int) and isinstance( - param2.physical_default_value, int): + param2.physical_default_value, int + ): append_list( "Default value", f"0x{param1.physical_default_value:0{(param1.get_static_bit_length() or 0) // 4}X}", @@ -285,45 +335,60 @@ def append_list( return {"Property": property, "Old Value": old, "New Value": new} - def compare_services(self, service1: DiagService, - service2: DiagService) -> List: # type: ignore[type-arg] + def compare_services(self, service1: DiagService, service2: DiagService) -> List: # type: ignore[type-arg] # compares request, positive response and negative response parameters of two diagnostic services - information: List[Union[str, Dict[str, Any]]] = [ - ] # information = [infotext1, table1, infotext2, table2, ...] + information: List[ + Union[str, Dict[str, Any]] + ] = [] # information = [infotext1, table1, infotext2, table2, ...] changed_params = "" # Request - if (service1.request is not None and service2.request is not None and - len(service1.request.parameters) == len(service2.request.parameters)): + if ( + service1.request is not None + and service2.request is not None + and len(service1.request.parameters) == len(service2.request.parameters) + ): for res1_idx, param1 in enumerate(service1.request.parameters): for res2_idx, param2 in enumerate(service2.request.parameters): if res1_idx == res2_idx: # find changed request parameter properties table = self.compare_parameters(param1, param2) - infotext = (f" Properties of request parameter '{param2.short_name}' " - f"that have changed:\n") + infotext = ( + f" Properties of request parameter '{param2.short_name}' " + f"that have changed:\n" + ) # array index starts with 0 -> param[0] is 1. service parameter if table["Property"]: information.append(infotext) information.append(table) - changed_params += (f"request parameter '{param2.short_name}',\n") + changed_params += ( + f"request parameter '{param2.short_name}',\n" + ) else: changed_params += "request parameter list, " # infotext - information.append(f"List of request parameters for service '{service2.short_name}' " - f"is not identical.\n") + information.append( + f"List of request parameters for service '{service2.short_name}' " + f"is not identical.\n" + ) # table - param_list1 = ([] if service1.request is None else service1.request.parameters) - param_list2 = ([] if service2.request is None else service2.request.parameters) + param_list1 = ( + [] if service1.request is None else service1.request.parameters + ) + param_list2 = ( + [] if service2.request is None else service2.request.parameters + ) - information.append({ - "List": ["Old list", "New list"], - "Values": [f"\\{param_list1}", f"\\{param_list2}"], - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [f"\\{param_list1}", f"\\{param_list2}"], + } + ) # Positive Responses if len(service1.positive_responses) == len(service2.positive_responses): @@ -332,13 +397,16 @@ def compare_services(self, service1: DiagService, if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate(response2.parameters): + for param2_idx, param2 in enumerate( + response2.parameters + ): if param1_idx == param2_idx: # find changed positive response parameter properties table = self.compare_parameters(param1, param2) infotext = ( f" Properties of positive response parameter '{param2.short_name}' that " - f"have changed:\n") + f"have changed:\n" + ) # array index starts with 0 -> param[0] is first service parameter if table["Property"]: @@ -352,26 +420,31 @@ def compare_services(self, service1: DiagService, f"List of positive response parameters for service '{service2.short_name}' is not identical." ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(response1.parameters), + str(response2.parameters), + ], + } + ) else: changed_params += "positive responses list, " # infotext information.append( - f"List of positive responses for service '{service2.short_name}' is not identical.") + f"List of positive responses for service '{service2.short_name}' is not identical." + ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [ - str(service1.positive_responses), - str(service2.positive_responses), - ], - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(service1.positive_responses), + str(service2.positive_responses), + ], + } + ) # Negative Responses if len(service1.negative_responses) == len(service2.negative_responses): @@ -380,7 +453,9 @@ def compare_services(self, service1: DiagService, if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate(response2.parameters): + for param2_idx, param2 in enumerate( + response2.parameters + ): if param1_idx == param2_idx: # find changed negative response parameter properties table = self.compare_parameters(param1, param2) @@ -398,13 +473,15 @@ def compare_services(self, service1: DiagService, f"List of positive response parameters for service '{service2.short_name}' is not identical.\n" ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(response1.parameters), + str(response2.parameters), + ], + } + ) else: changed_params += "negative responses list, " # infotext @@ -412,18 +489,20 @@ def compare_services(self, service1: DiagService, f"List of positive responses for service '{service2.short_name}' is not identical.\n" ) # table - information.append({ - "List": ["Old list", "New list"], - "Values": [ - str(service1.negative_responses), - str(service2.negative_responses), - ], - }) + information.append( + { + "List": ["Old list", "New list"], + "Values": [ + str(service1.negative_responses), + str(service2.negative_responses), + ], + } + ) return [information, changed_params] def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSpecs: - service_spec: ServiceSpecs = ServiceSpecs( + service_specs: ServiceSpecs = ServiceSpecs( diag_layer=dl1.short_name, diag_layer_type=dl1.variant_type.value, # list with added diagnostic services [service1, service2, service3, ...] Type: DiagService @@ -442,10 +521,12 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # extract the constant prefixes for the requests of all # services (used for duck-typed rename detection) dl1_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() for s in dl1.services + None if s.request is None else s.request.coded_const_prefix() + for s in dl1.services ] dl2_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() for s in dl2.services + None if s.request is None else s.request.coded_const_prefix() + for s in dl2.services ] # compare diagnostic services @@ -459,7 +540,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - service_spec.new_services.append(service1) + service_specs.new_services.append(service1) # check whether names of diagnostic services have changed elif service1 not in dl2.services: @@ -471,27 +552,34 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # save information about changes in dictionary # add new service (type: DiagService) - service_spec.changed_name_of_service.append((service2.short_name, service1)) + service_specs.changed_name_of_service.append( + (service2.short_name, service1) + ) # add old service name (type: String) - # service_spec.changed_name_of_service[1].append(service2.short_name) + # service_specs.changed_name_of_service[1].append(service2.short_name) # compare request, pos. response and neg. response parameters of diagnostic services detailed_information = self.compare_services(service1, service2) # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[1]: # check whether string "changed_params" is empty - service_spec.changed_parameters_of_service.append( - (detailed_information[1], detailed_information[0], service1)) + if detailed_information[ + 1 + ]: # check whether string "changed_params" is empty + service_specs.changed_parameters_of_service.append( + (detailed_information[1], detailed_information[0], service1) + ) for service2_idx, service2 in enumerate(dl2.services): # check for deleted diagnostic services - if (service2.short_name not in dl1_service_names and - dl2_request_prefixes[service2_idx] not in dl1_request_prefixes): - deleted_list = service_spec.deleted_services + if ( + service2.short_name not in dl1_service_names + and dl2_request_prefixes[service2_idx] not in dl1_request_prefixes + ): + deleted_list = service_specs.deleted_services assert isinstance(deleted_list, list) if service2 not in deleted_list: - service_spec.deleted_services.append(service2) + service_specs.deleted_services.append(service2) if service1.short_name == service2.short_name: # compare request, pos. response and neg. response parameters of both diagnostic services @@ -499,17 +587,24 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[1]: # check whether string "changed_params" is empty + if detailed_information[ + 1 + ]: # check whether string "changed_params" is empty # new service (type: DiagService) - service_spec.changed_parameters_of_service.append(service1) + service_specs.changed_parameters_of_service.append(service1) # add parameters which have been changed (type: String) - service_spec.changed_parameters_of_service.append(detailed_information[1]) + service_specs.changed_parameters_of_service.append( + detailed_information[1] + ) # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] - service_spec.changed_parameters_of_service.append(detailed_information[0]) - return service_spec + service_specs.changed_parameters_of_service.append( + detailed_information[0] + ) + return service_specs - def compare_databases(self, database_new: Database, - database_old: Database) -> SpecsChangesVariants: + def compare_databases( + self, database_new: Database, database_old: Database + ) -> SpecsChangesVariants: # compares two PDX-files with each other deleted_variants: list[DiagLayer] = [] @@ -529,17 +624,24 @@ def compare_databases(self, database_new: Database, for _, dl2 in enumerate(database_old.diag_layers): # check for deleted diagnostic layers - if (dl2.short_name not in [dl.short_name for dl in database_new.diag_layers] and - dl2 not in changes_variants.deleted_diagnostic_layers): + if ( + dl2.short_name + not in [dl.short_name for dl in database_new.diag_layers] + and dl2 not in changes_variants.deleted_diagnostic_layers + ): changes_variants.deleted_diagnostic_layers.append(dl2) - if (dl1.short_name == dl2.short_name and - dl1.short_name in self.diagnostic_layer_names): + if ( + dl1.short_name == dl2.short_name + and dl1.short_name in self.diagnostic_layer_names + ): # compare diagnostic services of both diagnostic layers # save diagnostic service changes in dictionary (empty if no changes) - service_spec: ServiceSpecs = self.compare_diagnostic_layers(dl1, dl2) - if service_spec: + service_specs: ServiceSpecs = self.compare_diagnostic_layers( + dl1, dl2 + ) + if service_specs: # adds information about diagnostic service changes to return variable (changes_variants) - changes_variants.service_changes[dl1.short_name] = service_spec + changes_variants.service_changes[dl1.short_name] = service_specs return changes_variants @@ -547,17 +649,19 @@ def compare_databases(self, database_new: Database, def add_subparser(subparsers: SubparsersList) -> None: parser = subparsers.add_parser( "compare", - description="\n".join([ - "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", - "", - "Examples:", - " Comparison of two diagnostic layers:", - " odxtools compare ./path/to/database.pdx -v variant1 variant2", - " Comparison of two database versions:", - " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", - " For more information use:", - " odxtools compare -h", - ]), + description="\n".join( + [ + "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", + "", + "Examples:", + " Comparison of two diagnostic layers:", + " odxtools compare ./path/to/database.pdx -v variant1 variant2", + " Comparison of two database versions:", + " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", + " For more information use:", + " odxtools compare -h", + ] + ), help="Compares two versions of diagnostic layers and/or databases with each other. Checks whether diagnostic services and its parameters have changed.", formatter_class=argparse.RawTextHelpFormatter, ) @@ -603,7 +707,9 @@ def run(args: argparse.Namespace) -> None: task = Comparison() task.param_detailed = args.no_details - db_names = [args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0])] + db_names = [ + args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0]) + ] if args.database and args.variants: # compare specified databases, consider only specified variants @@ -612,7 +718,9 @@ def run(args: argparse.Namespace) -> None: db_names.append(name) if isinstance(name, str) else str(name[0]) task.databases = [load_file(name) for name in db_names] - diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} + diag_layer_names = { + dl.short_name for db in task.databases for dl in db.diag_layers + } task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) @@ -633,22 +741,32 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") - print_dl_metrics([ - variant for variant in task.databases[0].diag_layers - if variant.short_name in task.diagnostic_layer_names - ]) + rich_print( + f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" + ) + print_dl_metrics( + [ + variant + for variant in task.databases[0].diag_layers + if variant.short_name in task.diagnostic_layer_names + ] + ) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})") - print_dl_metrics([ - variant for variant in task.databases[db_idx + 1].diag_layers - if variant.short_name in task.diagnostic_layer_names - ]) + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})" + ) + print_dl_metrics( + [ + variant + for variant in task.databases[db_idx + 1].diag_layers + if variant.short_name in task.diagnostic_layer_names + ] + ) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1])) + task.compare_databases(task.databases[0], task.databases[db_idx + 1]) + ) elif args.database: # compare specified databases, consider all variants @@ -659,9 +777,7 @@ def run(args: argparse.Namespace) -> None: # collect all diagnostic layers from all specified databases task.diagnostic_layer_names = { - dl.short_name - for db in task.databases - for dl in db.diag_layers + dl.short_name for db in task.databases for dl in db.diag_layers } task.db_indicator_1 = 0 @@ -675,16 +791,20 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") + rich_print( + f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" + ) print_dl_metrics(list(task.databases[0].diag_layers)) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})") + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})" + ) print_dl_metrics(list(task.databases[db_idx + 1].diag_layers)) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1])) + task.compare_databases(task.databases[0], task.databases[db_idx + 1]) + ) elif args.variants: # no databases specified -> comparison of diagnostic layers @@ -692,11 +812,15 @@ def run(args: argparse.Namespace) -> None: odxdb = _parser_utils.load_file(args) task.databases = [odxdb] - diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} + diag_layer_names = { + dl.short_name for db in task.databases for dl in db.diag_layers + } task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) task.diagnostic_layers = [ - dl for db in task.databases for dl in db.diag_layers + dl + for db in task.databases + for dl in db.diag_layers if dl.short_name in task.diagnostic_layer_names ] @@ -714,12 +838,15 @@ def run(args: argparse.Namespace) -> None: break rich_print() - rich_print(f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})") + rich_print( + f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})" + ) rich_print( f" (compared to '{task.diagnostic_layers[db_idx + 1].short_name}' ({task.diagnostic_layers[db_idx + 1].variant_type.value}))" ) task.print_dl_changes( - task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1])) + task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1]) + ) else: # no databases & no variants specified From e7bfc3041b653207798ab685bb7b75254f7b14b3 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Thu, 27 Mar 2025 23:51:11 +0000 Subject: [PATCH 10/20] Refractoring the code base with dataclass --- odxtools/cli/compare.py | 370 ++++++++++++++++++++-------------------- 1 file changed, 184 insertions(+), 186 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 18b30c3e..3bbb8102 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -3,8 +3,8 @@ import argparse import os -from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Set, Union from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -30,33 +30,41 @@ @dataclass -class ServiceSpecs: +class ChangedParameterDetails: + service: DiagService # The service whose parameters changed + changed_parameters: List[Any] = field(default_factory=list) # List of changed parameter names + change_details: List[Any] = field(default_factory=list) # Detailed change information + + +@dataclass +class ServiceDiff: diag_layer: str diag_layer_type: str - new_services: List[DiagService] # List of new service names - deleted_services: List[DiagService] # List of deleted services - renamed_service: list # type: ignore[type-arg] - changed_name_of_service: List[Tuple[str, DiagService]] - changed_parameters_of_service: list[Any, Any, DiagService] # type: ignore[type-arg] + new_services: List[DiagService] = field(default_factory=list) + deleted_services: List[DiagService] = field(default_factory=list) + changed_name_of_service: List[List[Union[str, DiagService]]] = field(default_factory=list) + changed_parameters_of_service: List[ChangedParameterDetails] = field(default_factory=list) @dataclass class SpecsChangesVariants: - new_diagnostic_layers: List[str] - deleted_diagnostic_layers: List[DiagLayer] - service_changes: Dict[str, ServiceSpecs] + new_diagnostic_layers: List[DiagLayer] = field(default_factory=list) + deleted_diagnostic_layers: List[DiagLayer] = field(default_factory=list) + # service_changes: Dict[str, ServiceDiff] = field(default_factory=dict) + service_changes: Dict[str, Union[List[DiagLayer], List[DiagLayer], + ServiceDiff]] = field(default_factory=dict) class Display: + param_detailed: bool obj_detailed: bool def __init__(self) -> None: pass - def print_dl_changes(self, service_spec: ServiceSpecs) -> None: - if (service_spec.new_services or service_spec.deleted_services or - service_spec.changed_name_of_service or service_spec.changed_parameters_of_service): + def print_dl_changes(self, service_spec: ServiceDiff) -> None: + if service_spec.new_services or service_spec.deleted_services or service_spec.changed_name_of_service or service_spec.changed_parameters_of_service: assert isinstance(service_spec.diag_layer, str) rich_print() rich_print( @@ -67,67 +75,71 @@ def print_dl_changes(self, service_spec: ServiceSpecs) -> None: rich_print() rich_print(" [blue]New services[/blue]") rich_print(extract_service_tabulation_data(service_spec.new_services)) - if service_spec.deleted_services: assert isinstance(service_spec.deleted_services, List) rich_print() rich_print(" [blue]Deleted services[/blue]") rich_print(extract_service_tabulation_data(service_spec.deleted_services)) - if service_spec.changed_name_of_service: - # assert isinstance(service_spec.changed_name_of_service[0], List) + if service_spec.changed_name_of_service[0]: rich_print() rich_print(" [blue]Renamed services[/blue]") - renamed_services_objects = [service for _, service in service_spec.renamed_service] - rich_print(extract_service_tabulation_data(renamed_services_objects)) - + rich_print( + extract_service_tabulation_data([ + item for sublist in service_spec.changed_name_of_service for item in sublist + if isinstance(item, DiagService) + ])) if service_spec.changed_parameters_of_service: - rich_print() - rich_print(" [blue]Services with parameter changes[/blue]") - # create table with information about services with parameter changes - changed_param_column = [str(x) for x in service_spec.changed_parameters_of_service] - table = extract_service_tabulation_data( - service_spec.changed_parameters_of_service, - additional_columns=[("Changed Parameters", changed_param_column)], - ) - rich_print(table) - - for service_idx, service in enumerate(service_spec.changed_parameters_of_service): - assert isinstance(service, DiagService) + first_change_details = service_spec.changed_parameters_of_service[0] + if first_change_details: rich_print() - rich_print( - f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" - ) - # detailed_info in [infotext1, dict1, infotext2, dict2, ...] - info_list = service_spec.changed_parameters_of_service[2][service_idx] - - for detailed_info in info_list: - if isinstance(detailed_info, str): - rich_print() - rich_print(detailed_info) - elif isinstance(detailed_info, dict): - table = RichTable( - show_header=True, - header_style="bold cyan", - border_style="blue", - show_lines=True, - ) - for header in detailed_info: - table.add_column(header) - rows = zip(*detailed_info.values()) - for row in rows: - table.add_row(*map(str, row)) - - rich_print(RichPadding(table, pad=(0, 0, 0, 4))) - rich_print() - if self.param_detailed: - # print all parameter details of diagnostic service - print_service_parameters(service, allow_unknown_bit_lengths=True) + rich_print(" [blue]Services with parameter changes[/blue]") + changed_param_column = [ + str(param_details.changed_parameters) + for param_details in service_spec.changed_parameters_of_service + ] + table = extract_service_tabulation_data([ + param_detail.service + for param_detail in service_spec.changed_parameters_of_service + ], + additional_columns=[("Changed Parameters", + changed_param_column)]) + rich_print(table) + for service_idx, param_detail in enumerate( + service_spec.changed_parameters_of_service): + service = param_detail.service + + assert isinstance(service, DiagService) + rich_print() + rich_print( + f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" + ) + + info_list = service_spec.changed_parameters_of_service[ + service_idx].change_details + for detailed_info in info_list: + if isinstance(detailed_info, str): + rich_print() + rich_print(detailed_info) + elif isinstance(detailed_info, dict): + table = RichTable( + show_header=True, + header_style="bold cyan", + border_style="blue", + show_lines=True) + for header in detailed_info: + table.add_column(header) + rows = zip(*detailed_info.values()) + for row in rows: + table.add_row(*map(str, row)) + + rich_print(RichPadding(table, pad=(0, 0, 0, 4))) + rich_print() + if self.param_detailed: + print_service_parameters(service, allow_unknown_bit_lengths=True) def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None: - # prints result of database comparison (input variable: dictionary: changes_variants) - # diagnostic layers - if (changes_variants.new_diagnostic_layers or changes_variants.deleted_diagnostic_layers): + if changes_variants.new_diagnostic_layers or changes_variants.deleted_diagnostic_layers: rich_print() rich_print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") rich_print(" New diagnostic layers: ") @@ -136,6 +148,15 @@ def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None rich_print( f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") rich_print(" Deleted diagnostic layers: ") + for variant in changes_variants.deleted_diagnostic_layers: + assert isinstance(variant, DiagLayer) + rich_print( + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") + + # diagnostic services + for _, value in changes_variants.service_changes.items(): + if isinstance(value, ServiceDiff): + self.print_dl_changes(value) class Comparison(Display): @@ -157,11 +178,8 @@ def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, old = [] new = [] - def append_list( - property_name: str, - new_property_value: Optional[AtomicOdxType], - old_property_value: Optional[AtomicOdxType], - ) -> None: + def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], + old_property_value: Optional[AtomicOdxType]) -> None: property.append(property_name) old.append(old_property_value) new.append(new_property_value) @@ -171,45 +189,36 @@ def append_list( if param1.byte_position != param2.byte_position: append_list("Byte position", param1.byte_position, param2.byte_position) if param1.get_static_bit_length() != param2.get_static_bit_length(): - append_list( - "Bit Length", - param1.get_static_bit_length(), - param2.get_static_bit_length(), - ) + append_list("Bit Length", param1.get_static_bit_length(), + param2.get_static_bit_length()) if param1.semantic != param2.semantic: append_list("Semantic", param1.semantic, param2.semantic) if param1.parameter_type != param2.parameter_type: append_list("Parameter type", param1.parameter_type, param2.parameter_type) if isinstance(param1, CodedConstParameter) and isinstance(param2, CodedConstParameter): - if (param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type): - append_list( - "Data type", - param1.diag_coded_type.base_data_type.name, - param2.diag_coded_type.base_data_type.name, - ) + if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type: + append_list("Data type", param1.diag_coded_type.base_data_type.name, + param2.diag_coded_type.base_data_type.name) if param1.coded_value != param2.coded_value: if isinstance(param1.coded_value, int) and isinstance(param2.coded_value, int): append_list( "Value", f"0x{param1.coded_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}", - ) + f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}") else: append_list("Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}") elif isinstance(param1, NrcConstParameter) and isinstance(param2, NrcConstParameter): - if (param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type): - append_list( - "Data type", - param1.diag_coded_type.base_data_type.name, - param2.diag_coded_type.base_data_type.name, - ) + if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type: + append_list("Data type", param1.diag_coded_type.base_data_type.name, + param2.diag_coded_type.base_data_type.name) if param1.coded_values != param2.coded_values: append_list("Values", str(param1.coded_values), str(param2.coded_values)) elif (dop_1 := getattr(param1, "dop", None)) is not None and (dop_2 := getattr( param2, "dop", None)) is not None: + if dop_1 != dop_2: # TODO: compare INTERNAL-CONSTR, COMPU-INTERNAL-TO-PHYS of DOP append_list("Linked DOP object", "", "") @@ -221,20 +230,11 @@ def append_list( # DOP Unit if getattr(dop_1, "unit", None) and getattr(dop_2, "unit", None): # (properties of unit object: short_name, long_name, description, odx_id, display_name, oid, factor_si_to_unit, offset_si_to_unit, physical_dimension_ref) - if (dop_1.unit != dop_2.unit and - dop_1.unit.short_name != dop_2.unit.short_name): - append_list( - " DOP unit name", - dop_1.unit.short_name, - dop_2.unit.short_name, - ) - elif (dop_1.unit != dop_2.unit and - dop_1.unit.display_name != dop_2.unit.display_name): - append_list( - " DOP unit display name", - dop_1.unit.display_name, - dop_2.unit.display_name, - ) + if dop_1.unit != dop_2.unit and dop_1.unit.short_name != dop_2.unit.short_name: + append_list(" DOP unit name", dop_1.unit.short_name, dop_2.unit.short_name) + elif dop_1.unit != dop_2.unit and dop_1.unit.display_name != dop_2.unit.display_name: + append_list(" DOP unit display name", dop_1.unit.display_name, + dop_2.unit.display_name) elif dop_1.unit != dop_2.unit: append_list(" DOP unit object", "", "") @@ -242,11 +242,9 @@ def append_list( if (dop_1.physical_type and dop_2.physical_type and dop_1.physical_type.base_data_type != dop_2.physical_type.base_data_type): - append_list( - " DOP physical data type", - dop_1.physical_type.base_data_type.name, - dop_2.physical_type.base_data_type.name, - ) + append_list(" DOP physical data type", + dop_1.physical_type.base_data_type.name, + dop_2.physical_type.base_data_type.name) if (isinstance(param1, PhysicalConstantParameter) and isinstance(param2, PhysicalConstantParameter) and @@ -256,14 +254,11 @@ def append_list( append_list( "Constant value", f"0x{param1.physical_constant_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.physical_constant_value:0{(param2.get_static_bit_length() or 0) // 4}X}", + f"0x{param2.physical_constant_value:0{(param2.get_static_bit_length() or 0) // 4}X}" ) else: - append_list( - "Constant value", - f"{param1.physical_constant_value!r}", - f"{param2.physical_constant_value!r}", - ) + append_list("Constant value", f"{param1.physical_constant_value!r}", + f"{param2.physical_constant_value!r}") elif (isinstance(param1, ValueParameter) and isinstance(param2, ValueParameter) and param1.physical_default_value is not None and @@ -274,19 +269,15 @@ def append_list( append_list( "Default value", f"0x{param1.physical_default_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.physical_default_value:0{(param2.get_static_bit_length() or 0) // 4}X}", + f"0x{param2.physical_default_value:0{(param2.get_static_bit_length() or 0) // 4}X}" ) else: - append_list( - "Default value", - f"{param1.physical_default_value!r}", - f"{param2.physical_default_value!r}", - ) + append_list("Default value", f"{param1.physical_default_value!r}", + f"{param2.physical_default_value!r}") return {"Property": property, "Old Value": old, "New Value": new} - def compare_services(self, service1: DiagService, - service2: DiagService) -> List: # type: ignore[type-arg] + def compare_services(self, service1: DiagService, service2: DiagService) -> List[ServiceDiff]: # compares request, positive response and negative response parameters of two diagnostic services information: List[Union[str, Dict[str, Any]]] = [ @@ -294,8 +285,8 @@ def compare_services(self, service1: DiagService, changed_params = "" # Request - if (service1.request is not None and service2.request is not None and - len(service1.request.parameters) == len(service2.request.parameters)): + if service1.request is not None and service2.request is not None and len( + service1.request.parameters) == len(service2.request.parameters): for res1_idx, param1 in enumerate(service1.request.parameters): for res2_idx, param2 in enumerate(service2.request.parameters): if res1_idx == res2_idx: @@ -308,7 +299,7 @@ def compare_services(self, service1: DiagService, if table["Property"]: information.append(infotext) information.append(table) - changed_params += (f"request parameter '{param2.short_name}',\n") + changed_params += f"request parameter '{param2.short_name}',\n" else: changed_params += "request parameter list, " # infotext @@ -317,12 +308,12 @@ def compare_services(self, service1: DiagService, # table - param_list1 = ([] if service1.request is None else service1.request.parameters) - param_list2 = ([] if service2.request is None else service2.request.parameters) + param_list1 = [] if service1.request is None else service1.request.parameters + param_list2 = [] if service2.request is None else service2.request.parameters information.append({ "List": ["Old list", "New list"], - "Values": [f"\\{param_list1}", f"\\{param_list2}"], + "Values": [f"\\{param_list1}", f"\\{param_list2}"] }) # Positive Responses @@ -354,10 +345,8 @@ def compare_services(self, service1: DiagService, # table information.append({ "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], + "Values": [str(response1.parameters), + str(response2.parameters)] }) else: changed_params += "positive responses list, " @@ -367,10 +356,8 @@ def compare_services(self, service1: DiagService, # table information.append({ "List": ["Old list", "New list"], - "Values": [ - str(service1.positive_responses), - str(service2.positive_responses), - ], + "Values": [str(service1.positive_responses), + str(service2.positive_responses)] }) # Negative Responses @@ -400,10 +387,8 @@ def compare_services(self, service1: DiagService, # table information.append({ "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], + "Values": [str(response1.parameters), + str(response2.parameters)] }) else: changed_params += "negative responses list, " @@ -414,33 +399,33 @@ def compare_services(self, service1: DiagService, # table information.append({ "List": ["Old list", "New list"], - "Values": [ - str(service1.negative_responses), - str(service2.negative_responses), - ], + "Values": [str(service1.negative_responses), + str(service2.negative_responses)] }) - return [information, changed_params] + return [information, changed_params] # type: ignore[list-item] - def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSpecs: - service_spec: ServiceSpecs = ServiceSpecs( + def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDiff: + # compares diagnostic services of two diagnostic layers with each other + # save changes in dictionary (service_dict) + # TODO: add comparison of SingleECUJobs + + new_services: List[DiagService] = [] + deleted_services: List[DiagService] = [] + renamed_service: List[List[Union[str, DiagService]]] = [[], + []] # List of (old_name, new_name) + services_with_param_changes: List[ChangedParameterDetails] = [ + ] # Parameter changes # TODO: implement list of tuples (str, str, DiagService)-tuples + + service_spec = ServiceDiff( diag_layer=dl1.short_name, diag_layer_type=dl1.variant_type.value, - # list with added diagnostic services [service1, service2, service3, ...] Type: DiagService - new_services=[], - # list with deleted diagnostic services [service1, service2, service3, ...] Type: DiagService - deleted_services=[], - # list with diagnostic services where the service name changed [[services], [old service names]] - renamed_service=[], - changed_name_of_service=[], - # list with diagnostic services where the service parameter changed [[services], [changed_parameters], [information_texts]] - changed_parameters_of_service=[], - ) - + new_services=new_services, + deleted_services=deleted_services, + changed_name_of_service=renamed_service, + changed_parameters_of_service=services_with_param_changes) dl1_service_names = [service.short_name for service in dl1.services] - # extract the constant prefixes for the requests of all - # services (used for duck-typed rename detection) dl1_request_prefixes: List[Optional[bytes]] = [ None if s.request is None else s.request.coded_const_prefix() for s in dl1.services ] @@ -450,6 +435,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # compare diagnostic services for service1 in dl1.services: + # check for added diagnostic services rq_prefix: Optional[bytes] = None if service1.request is not None: @@ -459,10 +445,12 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - service_spec.new_services.append(service1) + # service_spec.NewServices.append(service1) + service_spec.new_services.append(service1) # check whether names of diagnostic services have changed elif service1 not in dl2.services: + if rq_prefix is None or rq_prefix in dl2_request_prefixes: # get related diagnostic service for request service2_idx = dl2_request_prefixes.index(rq_prefix) @@ -471,9 +459,10 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # save information about changes in dictionary # add new service (type: DiagService) - service_spec.changed_name_of_service.append((service2.short_name, service1)) + + service_spec.changed_name_of_service[0].append(service1) # add old service name (type: String) - # service_spec.changed_name_of_service[1].append(service2.short_name) + service_spec.changed_name_of_service[1].append(service2.short_name) # compare request, pos. response and neg. response parameters of diagnostic services detailed_information = self.compare_services(service1, service2) @@ -481,13 +470,20 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # add information about changed diagnostic service parameters to dicitionary if detailed_information[1]: # check whether string "changed_params" is empty - service_spec.changed_parameters_of_service.append( - (detailed_information[1], detailed_information[0], service1)) + param_change_details = ChangedParameterDetails( + service=service1, + changed_parameters=[detailed_information[1]], + change_details=[detailed_information[0]], + ) + + service_spec.changed_parameters_of_service.append(param_change_details) for service2_idx, service2 in enumerate(dl2.services): + # check for deleted diagnostic services - if (service2.short_name not in dl1_service_names and - dl2_request_prefixes[service2_idx] not in dl1_request_prefixes): + if service2.short_name not in dl1_service_names and dl2_request_prefixes[ + service2_idx] not in dl1_request_prefixes: + deleted_list = service_spec.deleted_services assert isinstance(deleted_list, list) if service2 not in deleted_list: @@ -496,50 +492,51 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp if service1.short_name == service2.short_name: # compare request, pos. response and neg. response parameters of both diagnostic services detailed_information = self.compare_services(service1, service2) - # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary if detailed_information[1]: # check whether string "changed_params" is empty - # new service (type: DiagService) - service_spec.changed_parameters_of_service.append(service1) - # add parameters which have been changed (type: String) - service_spec.changed_parameters_of_service.append(detailed_information[1]) - # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] - service_spec.changed_parameters_of_service.append(detailed_information[0]) + param_change_details = ChangedParameterDetails( + service=service1, + changed_parameters=[detailed_information[1]], + change_details=[detailed_information[0]], + ) + service_spec.changed_parameters_of_service.append(param_change_details) + return service_spec def compare_databases(self, database_new: Database, database_old: Database) -> SpecsChangesVariants: # compares two PDX-files with each other - deleted_variants: list[DiagLayer] = [] + new_variants: List[DiagLayer] = [] # Assuming it stores diagnostic layer names + deleted_variants: List[DiagLayer] = [] - # Create the SpecsChangesVariants instance - changes_variants: SpecsChangesVariants = SpecsChangesVariants( - new_diagnostic_layers=[], + changes_variants = SpecsChangesVariants( + new_diagnostic_layers=new_variants, deleted_diagnostic_layers=deleted_variants, - service_changes={}, # You can populate this dictionary later as needed - ) + service_changes={}) # compare databases for _, dl1 in enumerate(database_new.diag_layers): # check for new diagnostic layers if dl1.short_name not in [dl.short_name for dl in database_old.diag_layers]: - changes_variants.new_diagnostic_layers.append(dl1.short_name) + changes_variants.new_diagnostic_layers.append(dl1) for _, dl2 in enumerate(database_old.diag_layers): # check for deleted diagnostic layers if (dl2.short_name not in [dl.short_name for dl in database_new.diag_layers] and dl2 not in changes_variants.deleted_diagnostic_layers): + changes_variants.deleted_diagnostic_layers.append(dl2) - if (dl1.short_name == dl2.short_name and - dl1.short_name in self.diagnostic_layer_names): + + if dl1.short_name == dl2.short_name and dl1.short_name in self.diagnostic_layer_names: # compare diagnostic services of both diagnostic layers # save diagnostic service changes in dictionary (empty if no changes) - service_spec: ServiceSpecs = self.compare_diagnostic_layers(dl1, dl2) - if service_spec: + service_spec: ServiceDiff = self.compare_diagnostic_layers(dl1, dl2) + # if isinstance(service_spec, ServiceDiff): + if changes_variants.service_changes is not None: # adds information about diagnostic service changes to return variable (changes_variants) - changes_variants.service_changes[dl1.short_name] = service_spec + changes_variants.service_changes.update({dl1.short_name: service_spec}) return changes_variants @@ -600,6 +597,7 @@ def add_subparser(subparsers: SubparsersList) -> None: def run(args: argparse.Namespace) -> None: + task = Comparison() task.param_detailed = args.no_details @@ -641,7 +639,7 @@ def run(args: argparse.Namespace) -> None: rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})") + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") print_dl_metrics([ variant for variant in task.databases[db_idx + 1].diag_layers if variant.short_name in task.diagnostic_layer_names @@ -680,7 +678,7 @@ def run(args: argparse.Namespace) -> None: rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})") + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") print_dl_metrics(list(task.databases[db_idx + 1].diag_layers)) task.print_database_changes( From 161bf4116ad62f8bf7bc1b65af52ab6207616f7e Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Fri, 28 Mar 2025 00:01:47 +0000 Subject: [PATCH 11/20] adding to remote branch --- odxtools/cli/compare.py | 699 ++++++++++++++++------------------------ 1 file changed, 285 insertions(+), 414 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 00d94bdd..3bbb8102 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -3,8 +3,8 @@ import argparse import os -from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Set, Union from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -22,137 +22,141 @@ from ..parameters.valueparameter import ValueParameter from . import _parser_utils from ._parser_utils import SubparsersList -from ._print_utils import ( - extract_service_tabulation_data, - print_dl_metrics, - print_service_parameters, -) +from ._print_utils import (extract_service_tabulation_data, print_dl_metrics, + print_service_parameters) # name of the tool _odxtools_tool_name_ = "compare" @dataclass -class ServiceSpecs: +class ChangedParameterDetails: + service: DiagService # The service whose parameters changed + changed_parameters: List[Any] = field(default_factory=list) # List of changed parameter names + change_details: List[Any] = field(default_factory=list) # Detailed change information + + +@dataclass +class ServiceDiff: diag_layer: str diag_layer_type: str - new_services: List[DiagService] # List of new service name - deleted_services: List[DiagService] # List of deleted services - renamed_service: list # type: ignore[type-arg] - changed_name_of_service: List[Tuple[str, DiagService]] - changed_parameters_of_service: list[Any, Any, DiagService] # type: ignore[type-arg] + new_services: List[DiagService] = field(default_factory=list) + deleted_services: List[DiagService] = field(default_factory=list) + changed_name_of_service: List[List[Union[str, DiagService]]] = field(default_factory=list) + changed_parameters_of_service: List[ChangedParameterDetails] = field(default_factory=list) @dataclass class SpecsChangesVariants: - new_diagnostic_layers: List[str] - deleted_diagnostic_layers: List[DiagLayer] - service_changes: Dict[str, ServiceSpecs] + new_diagnostic_layers: List[DiagLayer] = field(default_factory=list) + deleted_diagnostic_layers: List[DiagLayer] = field(default_factory=list) + # service_changes: Dict[str, ServiceDiff] = field(default_factory=dict) + service_changes: Dict[str, Union[List[DiagLayer], List[DiagLayer], + ServiceDiff]] = field(default_factory=dict) class Display: + param_detailed: bool obj_detailed: bool def __init__(self) -> None: pass - def print_dl_changes(self, service_specs: ServiceSpecs) -> None: - if ( - service_specs.new_services - or service_specs.deleted_services - or service_specs.changed_name_of_service - or service_specs.changed_parameters_of_service - ): - assert isinstance(service_specs.diag_layer, str) + def print_dl_changes(self, service_spec: ServiceDiff) -> None: + if service_spec.new_services or service_spec.deleted_services or service_spec.changed_name_of_service or service_spec.changed_parameters_of_service: + assert isinstance(service_spec.diag_layer, str) rich_print() rich_print( - f"Changed diagnostic services for diagnostic layer '{service_specs.diag_layer}' ({service_specs.diag_layer_type}):" + f"Changed diagnostic services for diagnostic layer '{service_spec.diag_layer}' ({service_spec.diag_layer_type}):" ) - if service_specs.new_services: - assert isinstance(service_specs.new_services, List) + if service_spec.new_services: + assert isinstance(service_spec.new_services, List) rich_print() rich_print(" [blue]New services[/blue]") - rich_print(extract_service_tabulation_data(service_specs.new_services)) - - if service_specs.deleted_services: - assert isinstance(service_specs.deleted_services, List) + rich_print(extract_service_tabulation_data(service_spec.new_services)) + if service_spec.deleted_services: + assert isinstance(service_spec.deleted_services, List) rich_print() rich_print(" [blue]Deleted services[/blue]") - rich_print(extract_service_tabulation_data(service_specs.deleted_services)) - if service_specs.changed_name_of_service: - # assert isinstance(service_specs.changed_name_of_service[0], List) + rich_print(extract_service_tabulation_data(service_spec.deleted_services)) + if service_spec.changed_name_of_service[0]: rich_print() rich_print(" [blue]Renamed services[/blue]") - renamed_services_objects = [ - service for _, service in service_specs.renamed_service - ] - rich_print(extract_service_tabulation_data(renamed_services_objects)) - - if service_specs.changed_parameters_of_service: - rich_print() - rich_print(" [blue]Services with parameter changes[/blue]") - # create table with information about services with parameter changes - changed_param_column = [ - str(x) for x in service_specs.changed_parameters_of_service - ] - table = extract_service_tabulation_data( - service_specs.changed_parameters_of_service, - additional_columns=[("Changed Parameters", changed_param_column)], - ) - rich_print(table) - - for service_idx, service in enumerate( - service_specs.changed_parameters_of_service - ): - assert isinstance(service, DiagService) + rich_print( + extract_service_tabulation_data([ + item for sublist in service_spec.changed_name_of_service for item in sublist + if isinstance(item, DiagService) + ])) + if service_spec.changed_parameters_of_service: + first_change_details = service_spec.changed_parameters_of_service[0] + if first_change_details: rich_print() - rich_print( - f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" - ) - # detailed_info in [infotext1, dict1, infotext2, dict2, ...] - info_list = service_specs.changed_parameters_of_service[2][service_idx] - - for detailed_info in info_list: - if isinstance(detailed_info, str): - rich_print() - rich_print(detailed_info) - elif isinstance(detailed_info, dict): - table = RichTable( - show_header=True, - header_style="bold cyan", - border_style="blue", - show_lines=True, - ) - for header in detailed_info: - table.add_column(header) - rows = zip(*detailed_info.values()) - for row in rows: - table.add_row(*map(str, row)) - - rich_print(RichPadding(table, pad=(0, 0, 0, 4))) - rich_print() - if self.param_detailed: - # print all parameter details of diagnostic service - print_service_parameters(service, allow_unknown_bit_lengths=True) + rich_print(" [blue]Services with parameter changes[/blue]") + changed_param_column = [ + str(param_details.changed_parameters) + for param_details in service_spec.changed_parameters_of_service + ] + table = extract_service_tabulation_data([ + param_detail.service + for param_detail in service_spec.changed_parameters_of_service + ], + additional_columns=[("Changed Parameters", + changed_param_column)]) + rich_print(table) + for service_idx, param_detail in enumerate( + service_spec.changed_parameters_of_service): + service = param_detail.service + + assert isinstance(service, DiagService) + rich_print() + rich_print( + f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" + ) + + info_list = service_spec.changed_parameters_of_service[ + service_idx].change_details + for detailed_info in info_list: + if isinstance(detailed_info, str): + rich_print() + rich_print(detailed_info) + elif isinstance(detailed_info, dict): + table = RichTable( + show_header=True, + header_style="bold cyan", + border_style="blue", + show_lines=True) + for header in detailed_info: + table.add_column(header) + rows = zip(*detailed_info.values()) + for row in rows: + table.add_row(*map(str, row)) + + rich_print(RichPadding(table, pad=(0, 0, 0, 4))) + rich_print() + if self.param_detailed: + print_service_parameters(service, allow_unknown_bit_lengths=True) def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None: - # prints result of database comparison (input variable: dictionary: changes_variants) - # diagnostic layers - if ( - changes_variants.new_diagnostic_layers - or changes_variants.deleted_diagnostic_layers - ): + if changes_variants.new_diagnostic_layers or changes_variants.deleted_diagnostic_layers: rich_print() rich_print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") rich_print(" New diagnostic layers: ") for variant in changes_variants.new_diagnostic_layers: assert isinstance(variant, DiagLayer) rich_print( - f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})" - ) + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") rich_print(" Deleted diagnostic layers: ") + for variant in changes_variants.deleted_diagnostic_layers: + assert isinstance(variant, DiagLayer) + rich_print( + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") + + # diagnostic services + for _, value in changes_variants.service_changes.items(): + if isinstance(value, ServiceDiff): + self.print_dl_changes(value) class Comparison(Display): @@ -166,9 +170,7 @@ class Comparison(Display): def __init__(self) -> None: pass - def compare_parameters( - self, param1: Parameter, param2: Parameter - ) -> Dict[str, Any]: + def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, Any]: # checks whether properties of param1 and param2 differ # checked properties: Name, Byte Position, Bit Length, Semantic, Parameter Type, Value (Coded, Constant, Default etc.), Data Type, Data Object Property (Name, Physical Data Type, Unit) @@ -176,11 +178,8 @@ def compare_parameters( old = [] new = [] - def append_list( - property_name: str, - new_property_value: Optional[AtomicOdxType], - old_property_value: Optional[AtomicOdxType], - ) -> None: + def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], + old_property_value: Optional[AtomicOdxType]) -> None: property.append(property_name) old.append(old_property_value) new.append(new_property_value) @@ -190,62 +189,36 @@ def append_list( if param1.byte_position != param2.byte_position: append_list("Byte position", param1.byte_position, param2.byte_position) if param1.get_static_bit_length() != param2.get_static_bit_length(): - append_list( - "Bit Length", - param1.get_static_bit_length(), - param2.get_static_bit_length(), - ) + append_list("Bit Length", param1.get_static_bit_length(), + param2.get_static_bit_length()) if param1.semantic != param2.semantic: append_list("Semantic", param1.semantic, param2.semantic) if param1.parameter_type != param2.parameter_type: append_list("Parameter type", param1.parameter_type, param2.parameter_type) - if isinstance(param1, CodedConstParameter) and isinstance( - param2, CodedConstParameter - ): - if ( - param1.diag_coded_type.base_data_type - != param2.diag_coded_type.base_data_type - ): - append_list( - "Data type", - param1.diag_coded_type.base_data_type.name, - param2.diag_coded_type.base_data_type.name, - ) + if isinstance(param1, CodedConstParameter) and isinstance(param2, CodedConstParameter): + if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type: + append_list("Data type", param1.diag_coded_type.base_data_type.name, + param2.diag_coded_type.base_data_type.name) if param1.coded_value != param2.coded_value: - if isinstance(param1.coded_value, int) and isinstance( - param2.coded_value, int - ): + if isinstance(param1.coded_value, int) and isinstance(param2.coded_value, int): append_list( "Value", f"0x{param1.coded_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}", - ) + f"0x{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}") else: - append_list( - "Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}" - ) + append_list("Value", f"{param1.coded_value!r}", f"{param2.coded_value!r}") - elif isinstance(param1, NrcConstParameter) and isinstance( - param2, NrcConstParameter - ): - if ( - param1.diag_coded_type.base_data_type - != param2.diag_coded_type.base_data_type - ): - append_list( - "Data type", - param1.diag_coded_type.base_data_type.name, - param2.diag_coded_type.base_data_type.name, - ) + elif isinstance(param1, NrcConstParameter) and isinstance(param2, NrcConstParameter): + if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type: + append_list("Data type", param1.diag_coded_type.base_data_type.name, + param2.diag_coded_type.base_data_type.name) if param1.coded_values != param2.coded_values: - append_list( - "Values", str(param1.coded_values), str(param2.coded_values) - ) + append_list("Values", str(param1.coded_values), str(param2.coded_values)) + + elif (dop_1 := getattr(param1, "dop", None)) is not None and (dop_2 := getattr( + param2, "dop", None)) is not None: - elif (dop_1 := getattr(param1, "dop", None)) is not None and ( - dop_2 := getattr(param2, "dop", None) - ) is not None: if dop_1 != dop_2: # TODO: compare INTERNAL-CONSTR, COMPU-INTERNAL-TO-PHYS of DOP append_list("Linked DOP object", "", "") @@ -257,138 +230,91 @@ def append_list( # DOP Unit if getattr(dop_1, "unit", None) and getattr(dop_2, "unit", None): # (properties of unit object: short_name, long_name, description, odx_id, display_name, oid, factor_si_to_unit, offset_si_to_unit, physical_dimension_ref) - if ( - dop_1.unit != dop_2.unit - and dop_1.unit.short_name != dop_2.unit.short_name - ): - append_list( - " DOP unit name", - dop_1.unit.short_name, - dop_2.unit.short_name, - ) - elif ( - dop_1.unit != dop_2.unit - and dop_1.unit.display_name != dop_2.unit.display_name - ): - append_list( - " DOP unit display name", - dop_1.unit.display_name, - dop_2.unit.display_name, - ) + if dop_1.unit != dop_2.unit and dop_1.unit.short_name != dop_2.unit.short_name: + append_list(" DOP unit name", dop_1.unit.short_name, dop_2.unit.short_name) + elif dop_1.unit != dop_2.unit and dop_1.unit.display_name != dop_2.unit.display_name: + append_list(" DOP unit display name", dop_1.unit.display_name, + dop_2.unit.display_name) elif dop_1.unit != dop_2.unit: append_list(" DOP unit object", "", "") if hasattr(dop_1, "physical_type") and hasattr(dop_2, "physical_type"): - if ( - dop_1.physical_type - and dop_2.physical_type - and dop_1.physical_type.base_data_type - != dop_2.physical_type.base_data_type - ): - append_list( - " DOP physical data type", - dop_1.physical_type.base_data_type.name, - dop_2.physical_type.base_data_type.name, - ) - - if ( - isinstance(param1, PhysicalConstantParameter) - and isinstance(param2, PhysicalConstantParameter) - and param1.physical_constant_value != param2.physical_constant_value - ): + if (dop_1.physical_type and dop_2.physical_type and + dop_1.physical_type.base_data_type + != dop_2.physical_type.base_data_type): + append_list(" DOP physical data type", + dop_1.physical_type.base_data_type.name, + dop_2.physical_type.base_data_type.name) + + if (isinstance(param1, PhysicalConstantParameter) and + isinstance(param2, PhysicalConstantParameter) and + param1.physical_constant_value != param2.physical_constant_value): if isinstance(param1.physical_constant_value, int) and isinstance( - param2.physical_constant_value, int - ): + param2.physical_constant_value, int): append_list( "Constant value", f"0x{param1.physical_constant_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.physical_constant_value:0{(param2.get_static_bit_length() or 0) // 4}X}", + f"0x{param2.physical_constant_value:0{(param2.get_static_bit_length() or 0) // 4}X}" ) else: - append_list( - "Constant value", - f"{param1.physical_constant_value!r}", - f"{param2.physical_constant_value!r}", - ) + append_list("Constant value", f"{param1.physical_constant_value!r}", + f"{param2.physical_constant_value!r}") - elif ( - isinstance(param1, ValueParameter) - and isinstance(param2, ValueParameter) - and param1.physical_default_value is not None - and param2.physical_default_value is not None - and param1.physical_default_value != param2.physical_default_value - ): + elif (isinstance(param1, ValueParameter) and isinstance(param2, ValueParameter) and + param1.physical_default_value is not None and + param2.physical_default_value is not None and + param1.physical_default_value != param2.physical_default_value): if isinstance(param1.physical_default_value, int) and isinstance( - param2.physical_default_value, int - ): + param2.physical_default_value, int): append_list( "Default value", f"0x{param1.physical_default_value:0{(param1.get_static_bit_length() or 0) // 4}X}", - f"0x{param2.physical_default_value:0{(param2.get_static_bit_length() or 0) // 4}X}", + f"0x{param2.physical_default_value:0{(param2.get_static_bit_length() or 0) // 4}X}" ) else: - append_list( - "Default value", - f"{param1.physical_default_value!r}", - f"{param2.physical_default_value!r}", - ) + append_list("Default value", f"{param1.physical_default_value!r}", + f"{param2.physical_default_value!r}") return {"Property": property, "Old Value": old, "New Value": new} - def compare_services(self, service1: DiagService, service2: DiagService) -> List: # type: ignore[type-arg] + def compare_services(self, service1: DiagService, service2: DiagService) -> List[ServiceDiff]: # compares request, positive response and negative response parameters of two diagnostic services - information: List[ - Union[str, Dict[str, Any]] - ] = [] # information = [infotext1, table1, infotext2, table2, ...] + information: List[Union[str, Dict[str, Any]]] = [ + ] # information = [infotext1, table1, infotext2, table2, ...] changed_params = "" # Request - if ( - service1.request is not None - and service2.request is not None - and len(service1.request.parameters) == len(service2.request.parameters) - ): + if service1.request is not None and service2.request is not None and len( + service1.request.parameters) == len(service2.request.parameters): for res1_idx, param1 in enumerate(service1.request.parameters): for res2_idx, param2 in enumerate(service2.request.parameters): if res1_idx == res2_idx: # find changed request parameter properties table = self.compare_parameters(param1, param2) - infotext = ( - f" Properties of request parameter '{param2.short_name}' " - f"that have changed:\n" - ) + infotext = (f" Properties of request parameter '{param2.short_name}' " + f"that have changed:\n") # array index starts with 0 -> param[0] is 1. service parameter if table["Property"]: information.append(infotext) information.append(table) - changed_params += ( - f"request parameter '{param2.short_name}',\n" - ) + changed_params += f"request parameter '{param2.short_name}',\n" else: changed_params += "request parameter list, " # infotext - information.append( - f"List of request parameters for service '{service2.short_name}' " - f"is not identical.\n" - ) + information.append(f"List of request parameters for service '{service2.short_name}' " + f"is not identical.\n") # table - param_list1 = ( - [] if service1.request is None else service1.request.parameters - ) - param_list2 = ( - [] if service2.request is None else service2.request.parameters - ) + param_list1 = [] if service1.request is None else service1.request.parameters + param_list2 = [] if service2.request is None else service2.request.parameters - information.append( - { - "List": ["Old list", "New list"], - "Values": [f"\\{param_list1}", f"\\{param_list2}"], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [f"\\{param_list1}", f"\\{param_list2}"] + }) # Positive Responses if len(service1.positive_responses) == len(service2.positive_responses): @@ -397,16 +323,13 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate( - response2.parameters - ): + for param2_idx, param2 in enumerate(response2.parameters): if param1_idx == param2_idx: # find changed positive response parameter properties table = self.compare_parameters(param1, param2) infotext = ( f" Properties of positive response parameter '{param2.short_name}' that " - f"have changed:\n" - ) + f"have changed:\n") # array index starts with 0 -> param[0] is first service parameter if table["Property"]: @@ -420,31 +343,22 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List f"List of positive response parameters for service '{service2.short_name}' is not identical." ) # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [str(response1.parameters), + str(response2.parameters)] + }) else: changed_params += "positive responses list, " # infotext information.append( - f"List of positive responses for service '{service2.short_name}' is not identical." - ) + f"List of positive responses for service '{service2.short_name}' is not identical.") # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(service1.positive_responses), - str(service2.positive_responses), - ], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [str(service1.positive_responses), + str(service2.positive_responses)] + }) # Negative Responses if len(service1.negative_responses) == len(service2.negative_responses): @@ -453,9 +367,7 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List if res1_idx == res2_idx: if len(response1.parameters) == len(response2.parameters): for param1_idx, param1 in enumerate(response1.parameters): - for param2_idx, param2 in enumerate( - response2.parameters - ): + for param2_idx, param2 in enumerate(response2.parameters): if param1_idx == param2_idx: # find changed negative response parameter properties table = self.compare_parameters(param1, param2) @@ -473,15 +385,11 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List f"List of positive response parameters for service '{service2.short_name}' is not identical.\n" ) # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(response1.parameters), - str(response2.parameters), - ], - } - ) + information.append({ + "List": ["Old list", "New list"], + "Values": [str(response1.parameters), + str(response2.parameters)] + }) else: changed_params += "negative responses list, " # infotext @@ -489,48 +397,45 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List f"List of positive responses for service '{service2.short_name}' is not identical.\n" ) # table - information.append( - { - "List": ["Old list", "New list"], - "Values": [ - str(service1.negative_responses), - str(service2.negative_responses), - ], - } - ) - - return [information, changed_params] - - def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSpecs: - service_specs: ServiceSpecs = ServiceSpecs( + information.append({ + "List": ["Old list", "New list"], + "Values": [str(service1.negative_responses), + str(service2.negative_responses)] + }) + + return [information, changed_params] # type: ignore[list-item] + + def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDiff: + # compares diagnostic services of two diagnostic layers with each other + # save changes in dictionary (service_dict) + # TODO: add comparison of SingleECUJobs + + new_services: List[DiagService] = [] + deleted_services: List[DiagService] = [] + renamed_service: List[List[Union[str, DiagService]]] = [[], + []] # List of (old_name, new_name) + services_with_param_changes: List[ChangedParameterDetails] = [ + ] # Parameter changes # TODO: implement list of tuples (str, str, DiagService)-tuples + + service_spec = ServiceDiff( diag_layer=dl1.short_name, diag_layer_type=dl1.variant_type.value, - # list with added diagnostic services [service1, service2, service3, ...] Type: DiagService - new_services=[], - # list with deleted diagnostic services [service1, service2, service3, ...] Type: DiagService - deleted_services=[], - # list with diagnostic services where the service name changed [[services], [old service names]] - renamed_service=[], - changed_name_of_service=[], - # list with diagnostic services where the service parameter changed [[services], [changed_parameters], [information_texts]] - changed_parameters_of_service=[], - ) - + new_services=new_services, + deleted_services=deleted_services, + changed_name_of_service=renamed_service, + changed_parameters_of_service=services_with_param_changes) dl1_service_names = [service.short_name for service in dl1.services] - # extract the constant prefixes for the requests of all - # services (used for duck-typed rename detection) dl1_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() - for s in dl1.services + None if s.request is None else s.request.coded_const_prefix() for s in dl1.services ] dl2_request_prefixes: List[Optional[bytes]] = [ - None if s.request is None else s.request.coded_const_prefix() - for s in dl2.services + None if s.request is None else s.request.coded_const_prefix() for s in dl2.services ] # compare diagnostic services for service1 in dl1.services: + # check for added diagnostic services rq_prefix: Optional[bytes] = None if service1.request is not None: @@ -540,10 +445,12 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - service_specs.new_services.append(service1) + # service_spec.NewServices.append(service1) + service_spec.new_services.append(service1) # check whether names of diagnostic services have changed elif service1 not in dl2.services: + if rq_prefix is None or rq_prefix in dl2_request_prefixes: # get related diagnostic service for request service2_idx = dl2_request_prefixes.index(rq_prefix) @@ -552,96 +459,84 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceSp # save information about changes in dictionary # add new service (type: DiagService) - service_specs.changed_name_of_service.append( - (service2.short_name, service1) - ) + + service_spec.changed_name_of_service[0].append(service1) # add old service name (type: String) - # service_specs.changed_name_of_service[1].append(service2.short_name) + service_spec.changed_name_of_service[1].append(service2.short_name) # compare request, pos. response and neg. response parameters of diagnostic services detailed_information = self.compare_services(service1, service2) # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[ - 1 - ]: # check whether string "changed_params" is empty - service_specs.changed_parameters_of_service.append( - (detailed_information[1], detailed_information[0], service1) + if detailed_information[1]: # check whether string "changed_params" is empty + param_change_details = ChangedParameterDetails( + service=service1, + changed_parameters=[detailed_information[1]], + change_details=[detailed_information[0]], ) + service_spec.changed_parameters_of_service.append(param_change_details) + for service2_idx, service2 in enumerate(dl2.services): + # check for deleted diagnostic services - if ( - service2.short_name not in dl1_service_names - and dl2_request_prefixes[service2_idx] not in dl1_request_prefixes - ): - deleted_list = service_specs.deleted_services + if service2.short_name not in dl1_service_names and dl2_request_prefixes[ + service2_idx] not in dl1_request_prefixes: + + deleted_list = service_spec.deleted_services assert isinstance(deleted_list, list) if service2 not in deleted_list: - service_specs.deleted_services.append(service2) + service_spec.deleted_services.append(service2) if service1.short_name == service2.short_name: # compare request, pos. response and neg. response parameters of both diagnostic services detailed_information = self.compare_services(service1, service2) - # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary - if detailed_information[ - 1 - ]: # check whether string "changed_params" is empty - # new service (type: DiagService) - service_specs.changed_parameters_of_service.append(service1) - # add parameters which have been changed (type: String) - service_specs.changed_parameters_of_service.append( - detailed_information[1] + if detailed_information[1]: # check whether string "changed_params" is empty + param_change_details = ChangedParameterDetails( + service=service1, + changed_parameters=[detailed_information[1]], + change_details=[detailed_information[0]], ) - # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...] - service_specs.changed_parameters_of_service.append( - detailed_information[0] - ) - return service_specs + service_spec.changed_parameters_of_service.append(param_change_details) + + return service_spec - def compare_databases( - self, database_new: Database, database_old: Database - ) -> SpecsChangesVariants: + def compare_databases(self, database_new: Database, + database_old: Database) -> SpecsChangesVariants: # compares two PDX-files with each other - deleted_variants: list[DiagLayer] = [] + new_variants: List[DiagLayer] = [] # Assuming it stores diagnostic layer names + deleted_variants: List[DiagLayer] = [] - # Create the SpecsChangesVariants instance - changes_variants: SpecsChangesVariants = SpecsChangesVariants( - new_diagnostic_layers=[], + changes_variants = SpecsChangesVariants( + new_diagnostic_layers=new_variants, deleted_diagnostic_layers=deleted_variants, - service_changes={}, # You can populate this dictionary later as needed - ) + service_changes={}) # compare databases for _, dl1 in enumerate(database_new.diag_layers): # check for new diagnostic layers if dl1.short_name not in [dl.short_name for dl in database_old.diag_layers]: - changes_variants.new_diagnostic_layers.append(dl1.short_name) + changes_variants.new_diagnostic_layers.append(dl1) for _, dl2 in enumerate(database_old.diag_layers): # check for deleted diagnostic layers - if ( - dl2.short_name - not in [dl.short_name for dl in database_new.diag_layers] - and dl2 not in changes_variants.deleted_diagnostic_layers - ): + if (dl2.short_name not in [dl.short_name for dl in database_new.diag_layers] and + dl2 not in changes_variants.deleted_diagnostic_layers): + changes_variants.deleted_diagnostic_layers.append(dl2) - if ( - dl1.short_name == dl2.short_name - and dl1.short_name in self.diagnostic_layer_names - ): + + if dl1.short_name == dl2.short_name and dl1.short_name in self.diagnostic_layer_names: # compare diagnostic services of both diagnostic layers # save diagnostic service changes in dictionary (empty if no changes) - service_specs: ServiceSpecs = self.compare_diagnostic_layers( - dl1, dl2 - ) - if service_specs: + service_spec: ServiceDiff = self.compare_diagnostic_layers(dl1, dl2) + # if isinstance(service_spec, ServiceDiff): + if changes_variants.service_changes is not None: # adds information about diagnostic service changes to return variable (changes_variants) - changes_variants.service_changes[dl1.short_name] = service_specs + changes_variants.service_changes.update({dl1.short_name: service_spec}) return changes_variants @@ -649,19 +544,17 @@ def compare_databases( def add_subparser(subparsers: SubparsersList) -> None: parser = subparsers.add_parser( "compare", - description="\n".join( - [ - "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", - "", - "Examples:", - " Comparison of two diagnostic layers:", - " odxtools compare ./path/to/database.pdx -v variant1 variant2", - " Comparison of two database versions:", - " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", - " For more information use:", - " odxtools compare -h", - ] - ), + description="\n".join([ + "Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.", + "", + "Examples:", + " Comparison of two diagnostic layers:", + " odxtools compare ./path/to/database.pdx -v variant1 variant2", + " Comparison of two database versions:", + " odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx", + " For more information use:", + " odxtools compare -h", + ]), help="Compares two versions of diagnostic layers and/or databases with each other. Checks whether diagnostic services and its parameters have changed.", formatter_class=argparse.RawTextHelpFormatter, ) @@ -704,12 +597,11 @@ def add_subparser(subparsers: SubparsersList) -> None: def run(args: argparse.Namespace) -> None: + task = Comparison() task.param_detailed = args.no_details - db_names = [ - args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0]) - ] + db_names = [args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0])] if args.database and args.variants: # compare specified databases, consider only specified variants @@ -718,9 +610,7 @@ def run(args: argparse.Namespace) -> None: db_names.append(name) if isinstance(name, str) else str(name[0]) task.databases = [load_file(name) for name in db_names] - diag_layer_names = { - dl.short_name for db in task.databases for dl in db.diag_layers - } + diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) @@ -741,32 +631,22 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" - ) - print_dl_metrics( - [ - variant - for variant in task.databases[0].diag_layers - if variant.short_name in task.diagnostic_layer_names - ] - ) + rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") + print_dl_metrics([ + variant for variant in task.databases[0].diag_layers + if variant.short_name in task.diagnostic_layer_names + ]) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})" - ) - print_dl_metrics( - [ - variant - for variant in task.databases[db_idx + 1].diag_layers - if variant.short_name in task.diagnostic_layer_names - ] - ) + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") + print_dl_metrics([ + variant for variant in task.databases[db_idx + 1].diag_layers + if variant.short_name in task.diagnostic_layer_names + ]) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1]) - ) + task.compare_databases(task.databases[0], task.databases[db_idx + 1])) elif args.database: # compare specified databases, consider all variants @@ -777,7 +657,9 @@ def run(args: argparse.Namespace) -> None: # collect all diagnostic layers from all specified databases task.diagnostic_layer_names = { - dl.short_name for db in task.databases for dl in db.diag_layers + dl.short_name + for db in task.databases + for dl in db.diag_layers } task.db_indicator_1 = 0 @@ -791,20 +673,16 @@ def run(args: argparse.Namespace) -> None: rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") rich_print() - rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})" - ) + rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") print_dl_metrics(list(task.databases[0].diag_layers)) rich_print() rich_print( - f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx + 1])})" - ) + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") print_dl_metrics(list(task.databases[db_idx + 1].diag_layers)) task.print_database_changes( - task.compare_databases(task.databases[0], task.databases[db_idx + 1]) - ) + task.compare_databases(task.databases[0], task.databases[db_idx + 1])) elif args.variants: # no databases specified -> comparison of diagnostic layers @@ -812,15 +690,11 @@ def run(args: argparse.Namespace) -> None: odxdb = _parser_utils.load_file(args) task.databases = [odxdb] - diag_layer_names = { - dl.short_name for db in task.databases for dl in db.diag_layers - } + diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers} task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants)) task.diagnostic_layers = [ - dl - for db in task.databases - for dl in db.diag_layers + dl for db in task.databases for dl in db.diag_layers if dl.short_name in task.diagnostic_layer_names ] @@ -838,15 +712,12 @@ def run(args: argparse.Namespace) -> None: break rich_print() - rich_print( - f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})" - ) + rich_print(f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})") rich_print( f" (compared to '{task.diagnostic_layers[db_idx + 1].short_name}' ({task.diagnostic_layers[db_idx + 1].variant_type.value}))" ) task.print_dl_changes( - task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1]) - ) + task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1])) else: # no databases & no variants specified From ef7660e9449fd07ef2400e24f270aacd58bb60a2 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Fri, 28 Mar 2025 00:21:12 +0000 Subject: [PATCH 12/20] Remove odxtools/cli/.gitignore from the repository --- odxtools/cli/.gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 odxtools/cli/.gitignore diff --git a/odxtools/cli/.gitignore b/odxtools/cli/.gitignore deleted file mode 100644 index 78141cd9..00000000 --- a/odxtools/cli/.gitignore +++ /dev/null @@ -1 +0,0 @@ -debug_log.txt From 9f1c890c75fea041a1cbb8648c47f0e1d22a29cf Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Fri, 28 Mar 2025 00:29:00 +0000 Subject: [PATCH 13/20] Removing the obselete codes --- odxtools/cli/compare.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 3bbb8102..7adaea93 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -50,7 +50,6 @@ class ServiceDiff: class SpecsChangesVariants: new_diagnostic_layers: List[DiagLayer] = field(default_factory=list) deleted_diagnostic_layers: List[DiagLayer] = field(default_factory=list) - # service_changes: Dict[str, ServiceDiff] = field(default_factory=dict) service_changes: Dict[str, Union[List[DiagLayer], List[DiagLayer], ServiceDiff]] = field(default_factory=dict) @@ -466,7 +465,6 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDi # compare request, pos. response and neg. response parameters of diagnostic services detailed_information = self.compare_services(service1, service2) - # detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params] # add information about changed diagnostic service parameters to dicitionary if detailed_information[1]: # check whether string "changed_params" is empty From 3e4d1b2b5eed8869f8a14761bb4e21219b2ba14d Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 30 Mar 2025 22:18:59 +0100 Subject: [PATCH 14/20] removing auto-generated non-source files --- odxtools/cli/.coverage | Bin 98304 -> 0 bytes .../cli/__pycache__/__init__.cpython-311.pyc | Bin 269 -> 0 bytes .../cli/__pycache__/__init__.cpython-312.pyc | Bin 252 -> 0 bytes .../__pycache__/_parser_utils.cpython-311.pyc | Bin 2158 -> 0 bytes .../__pycache__/_parser_utils.cpython-312.pyc | Bin 1958 -> 0 bytes .../__pycache__/_print_utils.cpython-311.pyc | Bin 17131 -> 0 bytes .../__pycache__/_print_utils.cpython-312.pyc | Bin 14822 -> 0 bytes odxtools/cli/__pycache__/browse.cpython-311.pyc | Bin 20473 -> 0 bytes odxtools/cli/__pycache__/browse.cpython-312.pyc | Bin 18165 -> 0 bytes .../cli/__pycache__/compare.cpython-311.pyc | Bin 40531 -> 0 bytes .../cli/__pycache__/compare.cpython-312.pyc | Bin 34247 -> 0 bytes odxtools/cli/__pycache__/decode.cpython-311.pyc | Bin 6507 -> 0 bytes odxtools/cli/__pycache__/decode.cpython-312.pyc | Bin 5729 -> 0 bytes .../dummy_sub_parser.cpython-311.pyc | Bin 2702 -> 0 bytes odxtools/cli/__pycache__/find.cpython-311.pyc | Bin 5972 -> 0 bytes odxtools/cli/__pycache__/find.cpython-312.pyc | Bin 5274 -> 0 bytes odxtools/cli/__pycache__/list.cpython-311.pyc | Bin 11508 -> 0 bytes odxtools/cli/__pycache__/list.cpython-312.pyc | Bin 10756 -> 0 bytes odxtools/cli/__pycache__/main.cpython-311.pyc | Bin 3369 -> 0 bytes odxtools/cli/__pycache__/snoop.cpython-311.pyc | Bin 14744 -> 0 bytes 20 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 odxtools/cli/.coverage delete mode 100644 odxtools/cli/__pycache__/__init__.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/__init__.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/_parser_utils.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/_parser_utils.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/_print_utils.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/_print_utils.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/browse.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/browse.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/compare.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/compare.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/decode.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/decode.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/dummy_sub_parser.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/find.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/find.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/list.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/list.cpython-312.pyc delete mode 100644 odxtools/cli/__pycache__/main.cpython-311.pyc delete mode 100644 odxtools/cli/__pycache__/snoop.cpython-311.pyc diff --git a/odxtools/cli/.coverage b/odxtools/cli/.coverage deleted file mode 100644 index 649b3c74f3f97b9e4e86f725f9912d537d7f2693..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98304 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 V_h{d-wSC9B1UC2Ie1?mG;y*Lh_A&qf diff --git a/odxtools/cli/__pycache__/_print_utils.cpython-311.pyc b/odxtools/cli/__pycache__/_print_utils.cpython-311.pyc deleted file mode 100644 index 93867c0fe4e17da11c7a0260406c9e4aeb9503fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17131 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 B1&ROw diff --git a/odxtools/cli/__pycache__/_print_utils.cpython-312.pyc b/odxtools/cli/__pycache__/_print_utils.cpython-312.pyc deleted file mode 100644 index 1e4811e4b3a8a478aeab32f9fae9f2830c47a8a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14822 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 bULVqzlf{dXrQ>hkTqhv8-1Y>$@WJ^XbNi-c diff --git a/odxtools/cli/__pycache__/browse.cpython-311.pyc b/odxtools/cli/__pycache__/browse.cpython-311.pyc deleted file mode 100644 index 66e3314510352b905b94cfea9c2dfde5ae15a2a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20473 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

OB;&298)7mL{^m4u#Al9Q#d_1k2YdO6MrxMKZa$ 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 F{{gEtpD+Lb diff --git a/odxtools/cli/__pycache__/compare.cpython-311.pyc b/odxtools/cli/__pycache__/compare.cpython-311.pyc deleted file mode 100644 index 28d3659f1870c2c1ed1264c6db3dee9d8f7f0688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40531 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 EUl{OrF#rGn diff --git a/odxtools/cli/__pycache__/compare.cpython-312.pyc b/odxtools/cli/__pycache__/compare.cpython-312.pyc deleted file mode 100644 index bf9575f8a35f8078b6c7e0504d2b7afc51691e6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34247 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 diff --git a/odxtools/cli/__pycache__/decode.cpython-311.pyc b/odxtools/cli/__pycache__/decode.cpython-311.pyc deleted file mode 100644 index 2e02d50308ba2b3bae522959a31a32e3d46f614f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6507 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 diff --git a/odxtools/cli/__pycache__/decode.cpython-312.pyc b/odxtools/cli/__pycache__/decode.cpython-312.pyc deleted file mode 100644 index eabd43aaccf25e454b4229edd48ed4f0d5eb3321..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5729 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 b9qLXuXJ1>W`Gu-qs3Z8;n0`nHcHw^lCjns= diff --git a/odxtools/cli/__pycache__/dummy_sub_parser.cpython-311.pyc b/odxtools/cli/__pycache__/dummy_sub_parser.cpython-311.pyc deleted file mode 100644 index ae20fa42eaa89e410fc0474f1dcfa9a61d204d83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2702 zcmZ`5OKcle@U6XTKRa% zlUPB@p%SUoLl1})+KQY)tMrQ00|yQqIB;-~C9IVqRYC}QbJcR`iJ7d^yrkw; zg|IkNZLO5ZClnGRrwP+86PDPH5wZhcIPyuR%#dvI0WNb{C7?6Du$+aD&OT6p0)(EK zN}+5Dj|=aN<@v>MUkD?k&cJ$;aDs+_5lTMJVq9f$uFb1Vy{zUFOyfzGU`be0SW}kb zsU_(ys%ml4!0BR1FsLg?ovf5ftFx|ao8N#b@993E`tqVxT-42?@98GIdf7IMe9^TT z7y7&_fHEq$X^)1|{-Wupbr?mrVl&-weZ9bS+cg^QI_0x5d}E5 zu!?lub-35|MsR1spVZOU7EP)_#b|P*T=zgN_~Es0L^sQ2ZVE6~>a^hy-JRdxAk|1y zU)2j>5QNNg!6lf{hWUz9L>Eol@>kJM)AxC)?1Pn|$KWZ`tuTbGvkTy;4$e(V-9ZYq z8;)7xfyRY!MKKcz6I@@sr%C>AACI?##FqQRMpjt^=72UW0?)+$S7pjC*r! z-TO$o(#O1AR##9fRigm)dx}u_+*Ik5Qxh4Y+)~$KcjKM4u8@NoBp*)OiB9a~v%@uD$XjCxdQ>o_|VjJ4hPP;V`=!2MA$y_xtiydU*$&m@n^@vysV}+!7u`rm`EA_+l{~{Q1R2AK$c7Q5O~zU|7|;doUo!0q52z^?WMT#b z=gs9=zT$rfCx6gBHyaljJf$B`QPIM7QHUNSWl(99jMc&Ybnak053=5m0M^M~@8Ff{ z)#`@&7tPdq&Rz5CgRjF3@6RWH`8dRCpUu_i#a()_PA>woM>E%^YIJCq4%O)pAVAnC zZH?FH#4eqv(+NQKXn&m^yFPXE?fUT4J&-=JOHb742|(`Cqkqujze5>y_tm>!Yz&Qj zpZPBHZST$AM*rB}$asC^t!9d(MjsL_m41w%*$G6S^d2JR9bRBn;CB|7ETSm=TSauB zsBWo#l+1)IMJaViVc{*e22;16Dl3_QTCSQog&lHQHhO(R48R6~Jw%}9Y(gAEd_P~} z-l`WQJRg4BM4<8&%g@Hbiyj9-4-M7z@P^}};#4#H_X^9GQRXxNxJ{aAbYHlBvOe-= zt@{GJzYhF19AbO@179C&YVlP1Ap!6hK@-8hl%z5n=dL8LCO70GbJZ|P zF00sxcN@kR71M5Ul7_+DB9P;rFM`yml_HlFBYMFefo(&KAD%20M93+p>X zo+SMe*efK!e+u4L0M?_Y8KW9)5(EP=ZDeb#NnpBd$?0i8Gg|s~u1R3J=gaA7@PelG zU8nV-S0JynzS|%K)Bl3dMl>%=r5oZUG<6t25VPE{17up2Y`~9@?S+0zkKRC8%vb5< z6Z%dV$9KgP&@dsrRRGPnqA2%C@49>%B)u-52I;G{-v$}1b-wpWw$}MJ6JrWE@+^3W Lw8w`qq-p;F#&)38 diff --git a/odxtools/cli/__pycache__/find.cpython-311.pyc b/odxtools/cli/__pycache__/find.cpython-311.pyc deleted file mode 100644 index c4aef6f5465f8ef1db8ec2f72a0ab6a77d38f996..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5972 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 diff --git a/odxtools/cli/__pycache__/find.cpython-312.pyc b/odxtools/cli/__pycache__/find.cpython-312.pyc deleted file mode 100644 index 9a0783f94c5c515589d3a8b13aa34b8afd9d9fd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5274 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 diff --git a/odxtools/cli/__pycache__/list.cpython-311.pyc b/odxtools/cli/__pycache__/list.cpython-311.pyc deleted file mode 100644 index b639728a0fb87c139e9f596b8e09424a028f5e10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11508 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 F{XhO^Ak+W= diff --git a/odxtools/cli/__pycache__/list.cpython-312.pyc b/odxtools/cli/__pycache__/list.cpython-312.pyc deleted file mode 100644 index e7a57dbdb92fd00777aa767b1fa46944106ca7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10756 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 Xs{QhG+DfOabOu3YY5Y0)$in_V`+MI| diff --git a/odxtools/cli/__pycache__/main.cpython-311.pyc b/odxtools/cli/__pycache__/main.cpython-311.pyc deleted file mode 100644 index b35a584308f8c9632b87db83b4e467fa8cd42bdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3369 zcmahLOKcm*b(Y*Emt1}&%2p}5TG^>>)}a(gQQO!~TD!6kHLexJF_6%5i4}K3twQdy zvqMW}B|wK9RKNw2poIk#1%w{d6w*P59&+d<1p*X3tppY#1`r^iC{W*M*ayR>zFBf9 z$%&oW**CxUn>X*hnZHD%VFcy9|NXs$LkRtc54<9GI*(ry5PE=aAsy*L8RgrpkQZR= zEBh+`yuTvmMWH<}1!l2-&&Liq?5vHyFXB!r`S@G^{a zc>>*;$;UznN7m!r#OGUe4)5utL7)cF+A=?RG&}K#Tql(So4I^ynvIzE>A+qHO#j7kwk^ zWBx1VJtkf^Z2FL^mlc>&DBQ)wHY~GHc*vE6xzI$dQn~lOWtCZk0xVQ4y;jC{Ho(L( z(6YcRu@-F1B-^yCDic34Oq~UET(oq|f<>!R)d+qj4A&!;zQ{S)!^N@zoP$-Z__0>P zEI=@=5wpk@^GXmpe)u0}06svpYcho9yM0|)?;6%4ScjO4spO0PcHkGeCyms-zZDZ?HgRIA9myg%`yB97;ScEmiQS$p25n;A=xF;q)yp_OXOG`-d(3wO^QgR! zT@N7!RS#v&C%yiBkJnokfvVRVMbNk77rOW%1SgR4@Q=1j{^xD8Bz_k+^TZkS-B?~g z^OSZI-CyD`o4oN%2xo)!4{lMTY)}K+Dz#L@q?l+$sxBCGPStAEs#w&xi&fpwN~UE~ zqp0fGE)t_kA>M6uR zsf2UoN^o3PEfo||u?Y!gNl;)^f#+-+x)=V8k&IIGn;yLZEVUcWBWC1ukI2RU#MXgq* zOeXl(HG^QCiF3GIt%q{CPOh$}-{ptl>f>!u?W|?}iC`sK4|SPl`j^HP4gR+RG>jj{6@mY+Ix)N=|a}J9hn`B{E2&b`Gu(KH^xn9Q-oHBe(~Q#h9jMG!K&DlBYSdsS5B{8+>y^V(bmm|Lo+z+eC=sLu91FY^_}ME zkN!674FBwRZ#N=?jmU+T&mVoOnMgg5KbO~1pT&M3`!sei`sN?xM&e8(G2$dfo^D?_ z9KGozI<*<;S)E*+Y{gOJa?9@txB2D%wbFNO)c5sh@1!I_{PBC{Mm9x;pk*PWQnd&z z8?!7x?^PiS68>Hx{5jF~=3dW-h7aAEWchM{g9`vyLgz0z9(aXztP}nyXh&bXP|!@% zqMGZPg#rt8;{{St67+q4*Y=gdu;j z*<%1(enAipP<*BHHBrLxY7=FgZ>wGOCQ3VAJwW5m@!CY^oa416qk&T^kyZWEvHM1w z^$yB3lMpPTFzy0~0iRzQZ_24Xd0GE`4=&@M>84N0|5j4`GaG^8f$< diff --git a/odxtools/cli/__pycache__/snoop.cpython-311.pyc b/odxtools/cli/__pycache__/snoop.cpython-311.pyc deleted file mode 100644 index 9da12083c83097f3ab0f22db95f19675596b76fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14744 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 From 22cef5b72e2c784787feccfb6101a10e06710ba8 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 30 Mar 2025 23:31:41 +0100 Subject: [PATCH 15/20] removing the Any type and removing the obselete lines --- odxtools/cli/compare.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 7adaea93..f01747ba 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -32,8 +32,9 @@ @dataclass class ChangedParameterDetails: service: DiagService # The service whose parameters changed - changed_parameters: List[Any] = field(default_factory=list) # List of changed parameter names - change_details: List[Any] = field(default_factory=list) # Detailed change information + changed_parameters: List[DiagService] = field( + default_factory=list) # List of changed parameter names + change_details: List[DiagService] = field(default_factory=list) # Detailed change information @dataclass @@ -50,8 +51,7 @@ class ServiceDiff: class SpecsChangesVariants: new_diagnostic_layers: List[DiagLayer] = field(default_factory=list) deleted_diagnostic_layers: List[DiagLayer] = field(default_factory=list) - service_changes: Dict[str, Union[List[DiagLayer], List[DiagLayer], - ServiceDiff]] = field(default_factory=dict) + service_changes: Dict[str, Union[List[DiagLayer], ServiceDiff]] = field(default_factory=dict) class Display: @@ -276,7 +276,7 @@ def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], return {"Property": property, "Old Value": old, "New Value": new} - def compare_services(self, service1: DiagService, service2: DiagService) -> List[ServiceDiff]: + def compare_services(self, service1: DiagService, service2: DiagService) -> List[DiagService]: # compares request, positive response and negative response parameters of two diagnostic services information: List[Union[str, Dict[str, Any]]] = [ @@ -444,7 +444,6 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDi if rq_prefix is None or rq_prefix not in dl2_request_prefixes: # TODO: this will not work in cases where the constant # prefix of a request was modified... - # service_spec.NewServices.append(service1) service_spec.new_services.append(service1) # check whether names of diagnostic services have changed @@ -531,7 +530,6 @@ def compare_databases(self, database_new: Database, # compare diagnostic services of both diagnostic layers # save diagnostic service changes in dictionary (empty if no changes) service_spec: ServiceDiff = self.compare_diagnostic_layers(dl1, dl2) - # if isinstance(service_spec, ServiceDiff): if changes_variants.service_changes is not None: # adds information about diagnostic service changes to return variable (changes_variants) changes_variants.service_changes.update({dl1.short_name: service_spec}) From 4940cfa9bd583633f65efa23be2e6ef7aecbabab Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Thu, 3 Apr 2025 01:26:58 +0100 Subject: [PATCH 16/20] adding the structured code base --- odxtools/cli/compare.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index f01747ba..0290d1ae 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -82,11 +82,12 @@ def print_dl_changes(self, service_spec: ServiceDiff) -> None: if service_spec.changed_name_of_service[0]: rich_print() rich_print(" [blue]Renamed services[/blue]") - rich_print( - extract_service_tabulation_data([ - item for sublist in service_spec.changed_name_of_service for item in sublist - if isinstance(item, DiagService) - ])) + tmp: List[DiagService] = [] + for sublist in service_spec.changed_name_of_service: + for item in sublist: + if isinstance(item, DiagService): + tmp.append(item) + rich_print(extract_service_tabulation_data(tmp)) if service_spec.changed_parameters_of_service: first_change_details = service_spec.changed_parameters_of_service[0] if first_change_details: @@ -96,12 +97,12 @@ def print_dl_changes(self, service_spec: ServiceDiff) -> None: str(param_details.changed_parameters) for param_details in service_spec.changed_parameters_of_service ] - table = extract_service_tabulation_data([ + services = [ param_detail.service for param_detail in service_spec.changed_parameters_of_service - ], - additional_columns=[("Changed Parameters", - changed_param_column)]) + ] + table = extract_service_tabulation_data( + services, additional_columns=[("Changed Parameters", changed_param_column)]) rich_print(table) for service_idx, param_detail in enumerate( service_spec.changed_parameters_of_service): From 157f76f816667673248ce18668976893dc9d4735 Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Thu, 3 Apr 2025 18:25:25 +0100 Subject: [PATCH 17/20] refactor the compare tool to reduce the number of ype: ignore comments --- .gitignore | 40 ---------------------------------------- odxtools/cli/compare.py | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 50 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index acfa5bb1..00000000 --- a/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# Distribution / packaging -*.egg-info/ - -# Coverage log -.coverage -*.lcov - -# IDEs -.vscode -.idea -/build/ -/dist/ -/bin/ -/include/ -/lib/ - -# Virtual Environments -.env -.venv -env -venv -/odxtools/version.py - -# editor and git backup files -*~ -*.orig -*.rej - -# files usually stemming from deflated PDX archives -*.odx-d -*.odx-c -*.odx-cs -*.jar -index.xml -!examples/data/* diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 0290d1ae..8852fd5a 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -4,7 +4,7 @@ import argparse import os from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Set, Union +from typing import Any, Dict, List, Optional, Union from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -76,6 +76,7 @@ def print_dl_changes(self, service_spec: ServiceDiff) -> None: rich_print(extract_service_tabulation_data(service_spec.new_services)) if service_spec.deleted_services: assert isinstance(service_spec.deleted_services, List) + rich_print() rich_print(" [blue]Deleted services[/blue]") rich_print(extract_service_tabulation_data(service_spec.deleted_services)) @@ -160,9 +161,9 @@ def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None class Comparison(Display): - databases: List[Database] = [] # storage of database objects - diagnostic_layers: List[DiagLayer] = [] # storage of DiagLayer objects - diagnostic_layer_names: Set[str] = set() # storage of diagnostic layer names + databases: list[Database] = [] # storage of database objects + diagnostic_layers: list[DiagLayer] = [] # storage of DiagLayer objects + diagnostic_layer_names: set[str] = set() # storage of diagnostic layer names db_indicator_1: int db_indicator_2: int @@ -170,7 +171,7 @@ class Comparison(Display): def __init__(self) -> None: pass - def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, Any]: + def compare_parameters(self, param1: Parameter, param2: Parameter) -> dict[str, Any]: # checks whether properties of param1 and param2 differ # checked properties: Name, Byte Position, Bit Length, Semantic, Parameter Type, Value (Coded, Constant, Default etc.), Data Type, Data Object Property (Name, Physical Data Type, Unit) @@ -178,8 +179,8 @@ def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, old = [] new = [] - def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], - old_property_value: Optional[AtomicOdxType]) -> None: + def append_list(property_name: str, new_property_value: AtomicOdxType | None, + old_property_value: AtomicOdxType | None) -> None: property.append(property_name) old.append(old_property_value) new.append(new_property_value) @@ -278,9 +279,10 @@ def append_list(property_name: str, new_property_value: Optional[AtomicOdxType], return {"Property": property, "Old Value": old, "New Value": new} def compare_services(self, service1: DiagService, service2: DiagService) -> List[DiagService]: + # compares request, positive response and negative response parameters of two diagnostic services - information: List[Union[str, Dict[str, Any]]] = [ + information: list[str | dict[str, Any]] = [ ] # information = [infotext1, table1, infotext2, table2, ...] changed_params = "" @@ -429,7 +431,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDi dl1_request_prefixes: List[Optional[bytes]] = [ None if s.request is None else s.request.coded_const_prefix() for s in dl1.services ] - dl2_request_prefixes: List[Optional[bytes]] = [ + dl2_request_prefixes: list[bytes | None] = [ None if s.request is None else s.request.coded_const_prefix() for s in dl2.services ] @@ -437,7 +439,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDi for service1 in dl1.services: # check for added diagnostic services - rq_prefix: Optional[bytes] = None + rq_prefix: bytes | None = None if service1.request is not None: rq_prefix = service1.request.coded_const_prefix() From 23f13235a6429a17af76f909094365dba0ef9a4c Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Tue, 15 Apr 2025 00:06:57 +0100 Subject: [PATCH 18/20] refactor the compare tool to reduce the number of ype: ignore comments --- odxtools/cli/compare.py | 71 ++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index 8852fd5a..6abb12f9 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -4,7 +4,7 @@ import argparse import os from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Union +from typing import Any from rich import print as rich_print from rich.padding import Padding as RichPadding @@ -32,26 +32,26 @@ @dataclass class ChangedParameterDetails: service: DiagService # The service whose parameters changed - changed_parameters: List[DiagService] = field( - default_factory=list) # List of changed parameter names - change_details: List[DiagService] = field(default_factory=list) # Detailed change information + changed_parameters: list[DiagService] = field( + default_factory=list) # list of changed parameter names + change_details: list[DiagService] = field(default_factory=list) # Detailed change information @dataclass class ServiceDiff: diag_layer: str diag_layer_type: str - new_services: List[DiagService] = field(default_factory=list) - deleted_services: List[DiagService] = field(default_factory=list) - changed_name_of_service: List[List[Union[str, DiagService]]] = field(default_factory=list) - changed_parameters_of_service: List[ChangedParameterDetails] = field(default_factory=list) + new_services: list[DiagService] = field(default_factory=list) + deleted_services: list[DiagService] = field(default_factory=list) + changed_name_of_service: list[list[str | DiagService]] = field(default_factory=list) + changed_parameters_of_service: list[ChangedParameterDetails] = field(default_factory=list) @dataclass class SpecsChangesVariants: - new_diagnostic_layers: List[DiagLayer] = field(default_factory=list) - deleted_diagnostic_layers: List[DiagLayer] = field(default_factory=list) - service_changes: Dict[str, Union[List[DiagLayer], ServiceDiff]] = field(default_factory=dict) + new_diagnostic_layers: list[DiagLayer] = field(default_factory=list) + deleted_diagnostic_layers: list[DiagLayer] = field(default_factory=list) + service_changes: dict[str, list[DiagLayer] | ServiceDiff] = field(default_factory=dict) class Display: @@ -70,20 +70,19 @@ def print_dl_changes(self, service_spec: ServiceDiff) -> None: f"Changed diagnostic services for diagnostic layer '{service_spec.diag_layer}' ({service_spec.diag_layer_type}):" ) if service_spec.new_services: - assert isinstance(service_spec.new_services, List) + assert isinstance(service_spec.new_services, list) rich_print() rich_print(" [blue]New services[/blue]") rich_print(extract_service_tabulation_data(service_spec.new_services)) if service_spec.deleted_services: - assert isinstance(service_spec.deleted_services, List) - + assert isinstance(service_spec.deleted_services, list) rich_print() rich_print(" [blue]Deleted services[/blue]") rich_print(extract_service_tabulation_data(service_spec.deleted_services)) if service_spec.changed_name_of_service[0]: rich_print() rich_print(" [blue]Renamed services[/blue]") - tmp: List[DiagService] = [] + tmp: list[DiagService] = [] for sublist in service_spec.changed_name_of_service: for item in sublist: if isinstance(item, DiagService): @@ -129,7 +128,7 @@ def print_dl_changes(self, service_spec: ServiceDiff) -> None: show_lines=True) for header in detailed_info: table.add_column(header) - rows = zip(*detailed_info.values()) + rows = zip(*detailed_info.values(), strict=True) for row in rows: table.add_row(*map(str, row)) @@ -278,8 +277,7 @@ def append_list(property_name: str, new_property_value: AtomicOdxType | None, return {"Property": property, "Old Value": old, "New Value": new} - def compare_services(self, service1: DiagService, service2: DiagService) -> List[DiagService]: - + def compare_services(self, service1: DiagService, service2: DiagService) -> list[DiagService]: # compares request, positive response and negative response parameters of two diagnostic services information: list[str | dict[str, Any]] = [ @@ -305,7 +303,7 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List else: changed_params += "request parameter list, " # infotext - information.append(f"List of request parameters for service '{service2.short_name}' " + information.append(f"list of request parameters for service '{service2.short_name}' " f"is not identical.\n") # table @@ -314,7 +312,7 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List param_list2 = [] if service2.request is None else service2.request.parameters information.append({ - "List": ["Old list", "New list"], + "list": ["Old list", "New list"], "Values": [f"\\{param_list1}", f"\\{param_list2}"] }) @@ -342,11 +340,11 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List changed_params += "positive response parameter list, " # infotext information.append( - f"List of positive response parameters for service '{service2.short_name}' is not identical." + f"list of positive response parameters for service '{service2.short_name}' is not identical." ) # table information.append({ - "List": ["Old list", "New list"], + "list": ["Old list", "New list"], "Values": [str(response1.parameters), str(response2.parameters)] }) @@ -354,10 +352,10 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List changed_params += "positive responses list, " # infotext information.append( - f"List of positive responses for service '{service2.short_name}' is not identical.") + f"list of positive responses for service '{service2.short_name}' is not identical.") # table information.append({ - "List": ["Old list", "New list"], + "list": ["Old list", "New list"], "Values": [str(service1.positive_responses), str(service2.positive_responses)] }) @@ -384,11 +382,11 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List changed_params += "positive response parameter list, " # infotext information.append( - f"List of positive response parameters for service '{service2.short_name}' is not identical.\n" + f"list of positive response parameters for service '{service2.short_name}' is not identical.\n" ) # table information.append({ - "List": ["Old list", "New list"], + "list": ["Old list", "New list"], "Values": [str(response1.parameters), str(response2.parameters)] }) @@ -396,11 +394,11 @@ def compare_services(self, service1: DiagService, service2: DiagService) -> List changed_params += "negative responses list, " # infotext information.append( - f"List of positive responses for service '{service2.short_name}' is not identical.\n" + f"list of positive responses for service '{service2.short_name}' is not identical.\n" ) # table information.append({ - "List": ["Old list", "New list"], + "list": ["Old list", "New list"], "Values": [str(service1.negative_responses), str(service2.negative_responses)] }) @@ -412,11 +410,10 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDi # save changes in dictionary (service_dict) # TODO: add comparison of SingleECUJobs - new_services: List[DiagService] = [] - deleted_services: List[DiagService] = [] - renamed_service: List[List[Union[str, DiagService]]] = [[], - []] # List of (old_name, new_name) - services_with_param_changes: List[ChangedParameterDetails] = [ + new_services: list[DiagService] = [] + deleted_services: list[DiagService] = [] + renamed_service: list[list[str | DiagService]] = [[], []] # list of (old_name, new_name) + services_with_param_changes: list[ChangedParameterDetails] = [ ] # Parameter changes # TODO: implement list of tuples (str, str, DiagService)-tuples service_spec = ServiceDiff( @@ -428,7 +425,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDi changed_parameters_of_service=services_with_param_changes) dl1_service_names = [service.short_name for service in dl1.services] - dl1_request_prefixes: List[Optional[bytes]] = [ + dl1_request_prefixes: list[bytes | None] = [ None if s.request is None else s.request.coded_const_prefix() for s in dl1.services ] dl2_request_prefixes: list[bytes | None] = [ @@ -439,7 +436,7 @@ def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> ServiceDi for service1 in dl1.services: # check for added diagnostic services - rq_prefix: bytes | None = None + rq_prefix: bytes if service1.request is not None: rq_prefix = service1.request.coded_const_prefix() @@ -508,8 +505,8 @@ def compare_databases(self, database_new: Database, database_old: Database) -> SpecsChangesVariants: # compares two PDX-files with each other - new_variants: List[DiagLayer] = [] # Assuming it stores diagnostic layer names - deleted_variants: List[DiagLayer] = [] + new_variants: list[DiagLayer] = [] # Assuming it stores diagnostic layer names + deleted_variants: list[DiagLayer] = [] changes_variants = SpecsChangesVariants( new_diagnostic_layers=new_variants, From 54556288652829e4e46869e5666c8d97f404e1ae Mon Sep 17 00:00:00 2001 From: vinothk-master Date: Sun, 20 Apr 2025 16:47:19 +0100 Subject: [PATCH 19/20] fixed by adding odxtools/version.py to tool.ruff.exclude in pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0f649756..64354a94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,7 @@ lint.ignore = [ exclude = [ "doc", + "odxtools/version.py", ] [tool.ruff.lint.isort] From 63890e0d6364cc8146888a01b726b537b015ec9b Mon Sep 17 00:00:00 2001 From: Andreas Lauser Date: Tue, 22 Apr 2025 09:10:17 +0200 Subject: [PATCH 20/20] attempt to fix the github actions for whatever reason, ruff starts to care about the `build/` subdirectory with https://github.com/mercedes-benz/odxtools/pull/393 . Since this is probably an artifact of pip, let's just delete this directory before linting. Signed-off-by: Andreas Lauser Signed-off-by: Christian Hackenbeck --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe146fde..b9548fc3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,6 +54,7 @@ jobs: pip install --upgrade pip pip install . pip install .[test] + rm -rf build - name: Static type checking (mypy) run: |