From 87f9bcfab7ef5633a278800ddd18e79c172783a9 Mon Sep 17 00:00:00 2001 From: Niclas Wesemann Date: Fri, 12 Apr 2024 12:57:55 +0200 Subject: [PATCH] add removal of nodes / branches v2 (#348) * add 'delete' element for node/branch removal Signed-off-by: Niclas Wesemann --- docs/vspec2x_arch.md | 100 +++++- tests/vspec/.gitignore | 1 + .../test_node_removal/test_files/test.vspec | 71 +++++ .../test_files/test_del_branch_overlay.vspec | 11 + .../test_del_instance_overlay.vspec | 11 + .../test_files/test_del_node_overlay.vspec | 12 + .../test_del_wrong_instance_overlay.vspec | 11 + .../test_files/test_deleted_branch.vspec | 72 +++++ .../test_files/test_deleted_node.vspec | 72 +++++ .../test_node_removal/test_node_removal.py | 279 +++++++++++++++++ tests/vspec/test_units.yaml | 20 ++ vspec/__init__.py | 7 + vspec/model/constants.py | 106 ++++--- vspec/model/vsstree.py | 296 ++++++++++++------ vspec/vssexporters/vss2ddsidl.py | 3 + vspec2binary.py | 9 +- vspec2csv.py | 9 +- vspec2ddsidl.py | 9 +- vspec2franca.py | 9 +- vspec2graphql.py | 9 +- vspec2json.py | 9 +- vspec2jsonschema.py | 9 +- vspec2protobuf.py | 8 +- vspec2yaml.py | 9 +- 24 files changed, 981 insertions(+), 171 deletions(-) create mode 100644 tests/vspec/test_node_removal/test_files/test.vspec create mode 100644 tests/vspec/test_node_removal/test_files/test_del_branch_overlay.vspec create mode 100644 tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec create mode 100644 tests/vspec/test_node_removal/test_files/test_del_node_overlay.vspec create mode 100644 tests/vspec/test_node_removal/test_files/test_del_wrong_instance_overlay.vspec create mode 100644 tests/vspec/test_node_removal/test_files/test_deleted_branch.vspec create mode 100644 tests/vspec/test_node_removal/test_files/test_deleted_node.vspec create mode 100644 tests/vspec/test_node_removal/test_node_removal.py diff --git a/docs/vspec2x_arch.md b/docs/vspec2x_arch.md index fbc69881..33043ccf 100644 --- a/docs/vspec2x_arch.md +++ b/docs/vspec2x_arch.md @@ -1,7 +1,5 @@ # vspec2x Design Decisions, Architecture and Limitations - - ## Overlays Each overlay is treated as a separate tree and merged (after expansion) on the existing tree. @@ -11,7 +9,6 @@ This means that descriptions, comments, datatype, unit and similar attributes ge Expansion is the process where a branch with instances is replaced with multiple branches. - This will result in that e.g. `Vehicle.Cabin.Door` is expanded to the following branches * `Vehicle.Cabin.Door.Row1.Left` @@ -22,11 +19,82 @@ This will result in that e.g. `Vehicle.Cabin.Door` is expanded to the following For some exporters expansion can be suppressed by using the `--no_expand` option. Then instance information will be represented by other means in the resulting output. +## Deletion / Node removal + +Nodes can be removed from the tree by using the `delete` element in the overlay. +This is useful when a signal is not used in a next version of the specification or if you +simply want to delete a node or branch from the specification, e.g. you are not using a signal of the base +specification. +Please note, that the deletion for branches will delete all nodes that are connected to that branch (which is +desired behavior). Also, if a branch node is deleted, all nodes that are connected to that branch will be deleted +irrespective of what their delete element value is. + +We chose three examples to show what you can do with the delete element. Let's say we have the following excerpt from +the base vehicle signal specification: + +```yaml +Vehicle.Service: + description: Service data. + type: branch + +Vehicle.Service.DistanceToService: + datatype: float + description: Remaining distance to service (of any kind). Negative values indicate service overdue. + type: sensor + unit: km + +Vehicle.Service.IsServiceDue: + datatype: boolean + description: Indicates if vehicle needs service (of any kind). True = Service needed now or in the near future. False = No known need for service. + type: sensor + +Vehicle.Service.TimeToService: + datatype: int32 + description: Remaining time to service (of any kind). Negative values indicate service overdue. + type: sensor + unit: s +``` + +Now if you want to delete the `Vehicle.Service.TimeToService` node from the specification, you can do this by adding the +delete element to your overlay like this: + +```yaml +Vehicle.Service.TimeToService: + type: sensor + datatype: int32 + delete: true +``` + +Let's say you now want to delete the whole branch `Vehicle.Service` from the specification. You can do this by adding: + +```yaml +Vehicle.Service: + type: branch + delete: true +``` + +Also, the delement element can be used on instances after they have been expanded. If you want to delete a node or +branch that has been expanded using instances you can add the `delete` element to the overlay, let's say the vehicle +only has two doors in the front. In this case we would like to delete the signals for the rear doors: + +```yaml +Vehicle.Cabin.Door.Row2: + type: branch + delete: true +``` + +By adding the `delete: true` to a node or branch all nodes and branches connected to it are deleted by vss-tools +when converting to a different format. + +Please note that for branches you need to provide at least the `type` element to +the overlay. For nodes you at least have to provide the `type` and `datatype` elements. Currently, the elements provided +do not have to match the previously given elements in the base specification. + ## Expansion and Overlays Sometimes an overlay only refers to a signal in a specific branch, like: -``` +```yaml Vehicle.Cabin.Door.Row2.Right.NewSignal: datatype: int8 type: sensor @@ -34,30 +102,35 @@ Vehicle.Cabin.Door.Row2.Right.NewSignal: description: A new signal for just one door. ``` -We do not want this signal expanded, and the tooling prevents expansion by taking the instance `Row2` and comparing with instances declared for `Door`. +We do not want this signal expanded, and the tooling prevents expansion by taking the instance `Row2` and comparing with +instances declared for `Door`. -``` +```yaml Door: type: branch instances: - Row[1,2] - - ["Left","Right"] + - [ "Left","Right" ] description: d1 comment: c1 ``` -It will do this by taking the instances on first level (`Row1` and `Row2`) and if comparing with current branch (`Row2`). -If they match it will repeat the check for `Right`and finally merge `NewSignal` with other signals in `Vehicle.Cabin.Door`. +It will do this by taking the instances on first level (`Row1` and `Row2`) and if comparing with current +branch (`Row2`). +If they match it will repeat the check for `Right`and finally merge `NewSignal` with other signals +in `Vehicle.Cabin.Door`. -Description, comments and other data defined for a specific instance (like `Vehicle.Cabin.Door.Row2.Right.NewSignal` above) have precedence +Description, comments and other data defined for a specific instance (like `Vehicle.Cabin.Door.Row2.Right.NewSignal` +above) have precedence over data defined for the unexpanded signal `Vehicle.Cabin.Door.NewSignal`. -The merge algorithm tries to be smart, so that if you use `Row*` it assume it is an instantiated branch if branch has an instance declaration of type `Row[m,n]`, -even if the the value of `Row*` is outside the range. It will however in that case not inherit values from the base branch. +The merge algorithm tries to be smart, so that if you use `Row*` it assume it is an instantiated branch if branch has an +instance declaration of type `Row[m,n]`, +even if the the value of `Row*` is outside the range. It will however in that case not inherit values from the base +branch. ## Linters and Static Code Checkers - ### MyPy [Mypy](https://mypy-lang.org/) is used for static type checking of code. @@ -80,7 +153,6 @@ Suppressed error categories include: and can thus not be analyzed. * Mypy does not like "method variables" like `load_flat_model.include_index` - ### Flake8 Flake8 is used as linter. It is also configured to be used as pre-commit hook. diff --git a/tests/vspec/.gitignore b/tests/vspec/.gitignore index 07e76579..50eef7d9 100644 --- a/tests/vspec/.gitignore +++ b/tests/vspec/.gitignore @@ -2,3 +2,4 @@ **/out.json **/out.txt **/out.vspec +**/out.yaml diff --git a/tests/vspec/test_node_removal/test_files/test.vspec b/tests/vspec/test_node_removal/test_files/test.vspec new file mode 100644 index 00000000..653e2b1f --- /dev/null +++ b/tests/vspec/test_node_removal/test_files/test.vspec @@ -0,0 +1,71 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0, which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +A: + type: branch + description: A is a test node +A.Float: + datatype: float + type: actuator + unit: mm + description: A.Float is a leaf of A of datatype float. +A.Int16: + datatype: int16 + type: sensor + unit: rpm + description: A.Int16 is a leaf of A of datatype int16. +A.String: + datatype: string + type: sensor + description: A.String is a leaf of A of datatype string. + deprecation: This is test deprecation, let's say it used to be called Str instead String. +A.StringArray: + datatype: string[] + type: sensor + description: A.StringArray is a leaf of A of datatype string array. +A.B: + type: branch + description: B is a branch of A. +A.B.Int32: + datatype: int32 + type: sensor + unit: rpm + description: A.B.Int32 is a leaf of A.B of datatype int32. +A.B.NewName: + datatype: uint32 + type: sensor + unit: mm + description: A.B.NewName's old name is 'OldName'. And its even older name is 'OlderName'. + fka: ['A.B.OlderName', 'A.B.OldName'] +A.B.IsLeaf: + datatype: string + type: actuator + allowed: ["YES", "NO"] + description: This node is a leaf of the tree and it has allowed values (aka an enum). +A.B.Min: + datatype: uint8 + type: sensor + min: 10 + unit: percent + description: A leaf that uses a minimum value. +A.B.Max: + datatype: uint8 + type: sensor + unit: percent + min: 0 + max: 100 + description: A leaf that uses a maximum value. +A.C: + type: branch + description: C is a branch of A. + instances: Instance[1,2] +A.C.Test: + datatype: uint32 + type: sensor + unit: mm + description: A.C.Test is a leaf of A.C of datatype uint32. diff --git a/tests/vspec/test_node_removal/test_files/test_del_branch_overlay.vspec b/tests/vspec/test_node_removal/test_files/test_del_branch_overlay.vspec new file mode 100644 index 00000000..0d690768 --- /dev/null +++ b/tests/vspec/test_node_removal/test_files/test_del_branch_overlay.vspec @@ -0,0 +1,11 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0, which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +A.B: + type: branch + delete: true diff --git a/tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec b/tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec new file mode 100644 index 00000000..5a34b90e --- /dev/null +++ b/tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec @@ -0,0 +1,11 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0, which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +A.C.Instance2: + type: branch + delete: true diff --git a/tests/vspec/test_node_removal/test_files/test_del_node_overlay.vspec b/tests/vspec/test_node_removal/test_files/test_del_node_overlay.vspec new file mode 100644 index 00000000..dbf4bc84 --- /dev/null +++ b/tests/vspec/test_node_removal/test_files/test_del_node_overlay.vspec @@ -0,0 +1,12 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0, which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +A.B.Int32: + type: sensor + datatype: int32 + delete: true diff --git a/tests/vspec/test_node_removal/test_files/test_del_wrong_instance_overlay.vspec b/tests/vspec/test_node_removal/test_files/test_del_wrong_instance_overlay.vspec new file mode 100644 index 00000000..7c3448b1 --- /dev/null +++ b/tests/vspec/test_node_removal/test_files/test_del_wrong_instance_overlay.vspec @@ -0,0 +1,11 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0, which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +A.C.Instance3: + type: branch + delete: true diff --git a/tests/vspec/test_node_removal/test_files/test_deleted_branch.vspec b/tests/vspec/test_node_removal/test_files/test_deleted_branch.vspec new file mode 100644 index 00000000..69357944 --- /dev/null +++ b/tests/vspec/test_node_removal/test_files/test_deleted_branch.vspec @@ -0,0 +1,72 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0, which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +A: + type: branch + description: A is a test node +A.Float: + datatype: float + type: actuator + unit: mm + description: A.Float is a leaf of A of datatype float. +A.Int16: + datatype: int16 + type: sensor + unit: rpm + description: A.Int16 is a leaf of A of datatype int16. +A.String: + datatype: string + type: sensor + description: A.String is a leaf of A of datatype string. + deprecation: This is test deprecation, let's say it used to be called Str instead String. +A.StringArray: + datatype: string[] + type: sensor + description: A.StringArray is a leaf of A of datatype string array. +A.B: + type: branch + description: B is a branch of A. + delete: true +A.B.Int32: + datatype: int32 + type: sensor + unit: rpm + description: A.B.Int32 is a leaf of A.B of datatype int32. +A.B.NewName: + datatype: uint32 + type: sensor + unit: mm + description: A.B.NewName's old name is 'OldName'. And its even older name is 'OlderName'. + fka: ['A.B.OlderName', 'A.B.OldName'] +A.B.IsLeaf: + datatype: string + type: actuator + allowed: ["YES", "NO"] + description: This node is a leaf of the tree and it has allowed values (aka an enum). +A.B.Min: + datatype: uint8 + type: sensor + min: 10 + unit: percent + description: A leaf that uses a minimum value. +A.B.Max: + datatype: uint8 + type: sensor + unit: percent + min: 0 + max: 100 + description: A leaf that uses a maximum value. +A.C: + type: branch + description: C is a branch of A. + instances: Instance[1,2] +A.C.Test: + datatype: uint32 + type: sensor + unit: mm + description: A.C.Test is a leaf of A.C of datatype uint32. diff --git a/tests/vspec/test_node_removal/test_files/test_deleted_node.vspec b/tests/vspec/test_node_removal/test_files/test_deleted_node.vspec new file mode 100644 index 00000000..03efca38 --- /dev/null +++ b/tests/vspec/test_node_removal/test_files/test_deleted_node.vspec @@ -0,0 +1,72 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0, which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +A: + type: branch + description: A is a test node. +A.Float: + datatype: float + type: actuator + unit: mm + description: A.Float is a leaf of A of datatype float. +A.Int16: + datatype: int16 + type: sensor + unit: rpm + description: A.Int16 is a leaf of A of datatype int16. +A.String: + datatype: string + type: sensor + description: A.String is a leaf of A of datatype string. + deprecation: This is test deprecation, let's say it used to be called Str instead String. +A.StringArray: + datatype: string[] + type: sensor + description: A.StringArray is a leaf of A of datatype string array. +A.B: + type: branch + description: B is a branch of A. +A.B.Int32: + datatype: int32 + type: sensor + unit: rpm + description: A.B.Int32 is a leaf of A.B of datatype int32. + delete: true +A.B.NewName: + datatype: uint32 + type: sensor + unit: mm + description: A.B.NewName's old name is 'OldName'. And its even older name is 'OlderName'. + fka: ['A.B.OlderName', 'A.B.OldName'] +A.B.IsLeaf: + datatype: string + type: actuator + allowed: ["YES", "NO"] + description: This node is a leaf of the tree and it has allowed values (aka an enum). +A.B.Min: + datatype: uint8 + type: sensor + min: 10 + unit: percent + description: A leaf that uses a minimum value. +A.B.Max: + datatype: uint8 + type: sensor + unit: percent + min: 0 + max: 100 + description: A leaf that uses a maximum value. +A.C: + type: branch + description: C is a branch of A. + instances: Instance[1,2] +A.C.Test: + datatype: uint32 + type: sensor + unit: mm + description: A.C.Test is a leaf of A.C of datatype uint32. diff --git a/tests/vspec/test_node_removal/test_node_removal.py b/tests/vspec/test_node_removal/test_node_removal.py new file mode 100644 index 00000000..b7360e46 --- /dev/null +++ b/tests/vspec/test_node_removal/test_node_removal.py @@ -0,0 +1,279 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +# +# Convert vspec files to various other formats +# + + +import os +import shlex +from typing import Optional + +import pytest + +import vspec2binary # noqa: F401 +import vspec2csv # noqa: F401 +import vspec2ddsidl # noqa: F401 +import vspec2franca # noqa: F401 +import vspec2graphql # noqa: F401 +import vspec2json # noqa: F401 +import vspec2jsonschema # noqa: F401 +import vspec2protobuf # noqa: F401 +import vspec2yaml # noqa: F401 + + +# HELPERS + + +def get_cla(test_file: str, out_file: str, overlay: Optional[str]): + if overlay: + return test_file + " " + out_file + " -o " + overlay + " -u ../test_units.yaml" + else: + return test_file + " " + out_file + " -u ../test_units.yaml" + + +# FIXTURES + + +@pytest.fixture +def change_test_dir(request, monkeypatch): + # To make sure we run from test directory + monkeypatch.chdir(request.fspath.dirname) + + +# Please note that tests will possibly fail if the out.* files are not deleted. +# They will also remain if a test fails which makes it easier to understand why +# it failed. Please remove all out.* before you run the tests again. +@pytest.fixture(scope="function", autouse=True) +def delete_files(change_test_dir): + yield None + os.system("rm -f out.*") + + +# INTEGRATION TESTS + + +@pytest.mark.usefixtures("change_test_dir") +@pytest.mark.parametrize( + "exporter, out_file", + [ + ("vspec2binary", "out.bin"), + ("vspec2csv", "out.csv"), + ("vspec2ddsidl", "out.idl"), + ("vspec2franca", "out.fidl"), + ("vspec2graphql", "out.graphql"), + ("vspec2json", "out.json"), + ("vspec2jsonschema", "out.jsonschema"), + ("vspec2protobuf", "out.pb"), + ("vspec2yaml", "out.yaml"), + ], +) +@pytest.mark.parametrize( + "overlay", + [ + None, + "test_files/test_del_node_overlay.vspec", + ], +) +def test_deleted_node(exporter: str, out_file: str, overlay: Optional[str]): + test_file: str + if overlay: + test_file = "test_files/test.vspec" + else: + test_file = "test_files/test_deleted_node.vspec" + + clas = shlex.split( + get_cla(test_file, out_file, overlay) + ) # get command line arguments without the executable + + eval(f"{exporter}.main({clas})") + with open(out_file) as f: + result = f.read() + + remaining_nodes = [ + "A.Float", + "A.Int16", + "A.String", + "A.StringArray", + "A.B", + "A.B.NewName", + "A.B.IsLeaf", + "A.B.Min", + "A.B.Max", + "A.C", + "A.C.Instance1", + "A.C.Instance1.Test", + "A.C.Instance2", + "A.C.Instance2.Test", + ] + if exporter in [ + "vspec2binary", + "vspec2ddsidl", + "vspec2json", + "vspec2jsonschema", + "vspec2protobuf", + ]: + assert "A.B.Int32".split(".")[-1] not in result + remaining_nodes = [node.split(".")[-1] for node in remaining_nodes] + for node in remaining_nodes: + assert node in result + if exporter == "vspec2graphql": + assert "A.B.Int32".replace(".", "_") not in result + remaining_nodes = [node.replace(".", "_") for node in remaining_nodes] + for node in remaining_nodes: + assert node in result + else: + assert "A.B.Int32" not in result + for node in remaining_nodes: + assert node in result + + +@pytest.mark.usefixtures("change_test_dir") +@pytest.mark.parametrize( + "exporter, out_file", + [ + ("vspec2binary", "out.bin"), + ("vspec2csv", "out.csv"), + ("vspec2ddsidl", "out.idl"), + ("vspec2franca", "out.fidl"), + ("vspec2graphql", "out.graphql"), + ("vspec2json", "out.json"), + ("vspec2jsonschema", "out.jsonschema"), + ("vspec2protobuf", "out.pb"), + ("vspec2yaml", "out.yaml"), + ], +) +@pytest.mark.parametrize( + "overlay", + [ + None, + "test_files/test_del_branch_overlay.vspec", + ], +) +def test_deleted_branch(exporter: str, out_file: str, overlay: Optional[str]): + test_file: str + if overlay: + test_file = "test_files/test.vspec" + else: + test_file = "test_files/test_deleted_branch.vspec" + clas = shlex.split(get_cla(test_file, out_file, overlay)) + + eval(f"{exporter}.main({clas})") + result_file: str + with open(out_file) as f: + result_file = f.read() + + remaining_nodes = [ + "A.Float", + "A.Int16", + "A.String", + "A.StringArray", + "A.C", + "A.C.Instance1", + "A.C.Instance1.Test", + "A.C.Instance2", + "A.C.Instance2.Test", + ] + + if exporter in [ + "vspec2binary", + "vspec2ddsidl", + "vspec2json", + "vspec2jsonschema", + "vspec2protobuf", + ]: + assert "A.B".split(".")[-1] not in result_file + remaining_nodes = [node.split(".")[-1] for node in remaining_nodes] + for node in remaining_nodes: + assert node in result_file + elif exporter == "vspec2graphql": + assert "A.B".replace(".", "_") not in result_file + remaining_nodes = [node.replace(".", "_") for node in remaining_nodes] + for node in remaining_nodes: + assert node in result_file + else: + assert "A.B" not in result_file + for node in remaining_nodes: + assert node in result_file + + +@pytest.mark.usefixtures("change_test_dir") +@pytest.mark.parametrize( + "exporter, out_file", + [ + ("vspec2binary", "out.bin"), + ("vspec2csv", "out.csv"), + ("vspec2ddsidl", "out.idl"), + ("vspec2franca", "out.fidl"), + ("vspec2graphql", "out.graphql"), + ("vspec2json", "out.json"), + ("vspec2jsonschema", "out.jsonschema"), + ("vspec2protobuf", "out.pb"), + ("vspec2yaml", "out.yaml"), + ], +) +@pytest.mark.parametrize( + "overlay", + [ + "test_files/test_del_instance_overlay.vspec", + "test_files/test_del_wrong_instance_overlay.vspec", + ], +) +def test_deleted_instance( + caplog: pytest.LogCaptureFixture, exporter: str, out_file: str, overlay: str +): + test_file: str = "test_files/test.vspec" + clas = shlex.split(get_cla(test_file, out_file, overlay)) + + if "wrong" in overlay: + with pytest.raises(SystemExit) as exporter_result: + eval(f"{exporter}.main({clas})") + assert exporter_result.type == SystemExit + assert exporter_result.value.code == -1 + else: + eval(f"{exporter}.main({clas})") + result_file: str + with open(out_file) as f: + result_file = f.read() + + remaining_nodes = [ + "A.Float", + "A.Int16", + "A.String", + "A.StringArray", + "A.B", + "A.B.NewName", + "A.B.IsLeaf", + "A.B.Min", + "A.B.Max", + "A.C", + "A.C.Instance1", + "A.C.Instance1.Test", + ] + + if exporter in [ + "vspec2binary", + "vspec2ddsidl", + "vspec2json", + "vspec2jsonschema", + "vspec2protobuf", + ]: + assert "A.C.Instance2".split(".")[-1] not in result_file + remaining_nodes = [node.split(".")[-1] for node in remaining_nodes] + for node in remaining_nodes: + assert node in result_file + elif exporter == "vspec2graphql": + assert "A.C.Instance2".replace(".", "_") not in result_file + remaining_nodes = [node.replace(".", "_") for node in remaining_nodes] + for node in remaining_nodes: + assert node in result_file + else: + assert "A.C.Instance2" not in result_file + for node in remaining_nodes: + assert node in result_file diff --git a/tests/vspec/test_units.yaml b/tests/vspec/test_units.yaml index f6d59735..fd685725 100644 --- a/tests/vspec/test_units.yaml +++ b/tests/vspec/test_units.yaml @@ -10,3 +10,23 @@ celsius: unit: degree celsius quantity: temperature allowed-datatypes: ['numeric'] +mm: + label: millimeter + description: Distance measured in millimeters + domain: distance +m: + label: meter + description: Distance measured in meters + domain: distance +degrees/s: + label: degree per second + description: Angular speed measured in degrees per second + domain: angular speed +rpm: + label: revolutions per minute + description: Rotational speed measured in revolutions per minute + domain: rotational speed +percent: + label: percent + description: Relation measured in percent + domain: relation diff --git a/vspec/__init__.py b/vspec/__init__.py index 2bf4de5d..9b67d654 100755 --- a/vspec/__init__.py +++ b/vspec/__init__.py @@ -298,6 +298,13 @@ def verify_mandatory_attributes(node, abort_on_unknown_attribute: bool): mandatory in individual files but only in the final tree """ if isinstance(node, VSSNode): + if node.delete: + logging.info( + f"Node {node.qualified_name()} will be deleted. Please note, that if {node.qualified_name()} " + f"is a branch all subsequent nodes will also be deleted irrespective of their 'delete' value." + ) + node.parent = None + node.children = [] node.verify_attributes(abort_on_unknown_attribute) for child in node.children: verify_mandatory_attributes(child, abort_on_unknown_attribute) diff --git a/vspec/model/constants.py b/vspec/model/constants.py index 5a7197a8..a076492a 100644 --- a/vspec/model/constants.py +++ b/vspec/model/constants.py @@ -16,14 +16,14 @@ import re import logging import sys -from enum import Enum, EnumMeta -from typing import ( - Sequence, Type, TypeVar, Optional, Dict, TextIO, List -) from collections import abc +from collections.abc import Sequence +from enum import Enum, EnumMeta +from typing import TextIO, TypeVar import yaml +NON_ALPHANUMERIC_WORD = re.compile("[^A-Za-z0-9]+") NON_ALPHANUMERIC_WORD = re.compile('[^A-Za-z0-9]+') @@ -31,16 +31,22 @@ class VSSUnit(str): - """String subclass for storing unit information. - """ - id: str # Typically abbreviation like "V" - unit: Optional[str] = None # Typically full name like "Volt" - definition: Optional[str] = None - quantity: Optional[str] = None # Typically quantity, like "Voltage" - allowed_datatypes: Optional[List[str]] = None # Typically quantity, like "Voltage" + """String subclass for storing unit information.""" - def __new__(cls, id: str, unit: Optional[str] = None, definition: Optional[str] = None, - quantity: Optional[str] = None, allowed_datatypes: Optional[List[str]] = None) -> VSSUnit: + id: str # Typically abbreviation like "V" + unit: str | None = None # Typically full name like "Volt" + definition: str | None = None + quantity: str | None = None # Typically quantity, like "Voltage" + allowed_datatypes: list[str] | None = None # Typically quantity, like "Voltage" + + def __new__( + cls, + id: str, + unit: str | None = None, + definition: str | None = None, + quantity: str | None = None, + allowed_datatypes: list[str] | None = None, + ) -> VSSUnit: self = super().__new__(cls, id) self.id = id self.unit = unit @@ -55,15 +61,20 @@ def value(self): class VSSQuantity(str): - """String subclass for storing quantity information. - """ + """String subclass for storing quantity information.""" + id: str # Identifier preferably taken from a standard, like ISO 80000 definition: str # Explanation of quantity, for example reference to standard - remark: Optional[str] = None # remark as defined in for example ISO 80000 - comment: Optional[str] = None - - def __new__(cls, id: str, definition: str, remark: Optional[str] = None, - comment: Optional[str] = None) -> VSSQuantity: + remark: str | None = None # remark as defined in for example ISO 80000 + comment: str | None = None + + def __new__( + cls, + id: str, + definition: str, + remark: str | None = None, + comment: str | None = None, + ) -> VSSQuantity: self = super().__new__(cls, id) self.id = id self.definition = definition @@ -78,23 +89,22 @@ def value(self): class EnumMetaWithReverseLookup(EnumMeta): """This class extends EnumMeta and adds: - - from_str(str): reverse lookup - - values(): sequence of values + - from_str(str): reverse lookup + - values(): sequence of values """ + def __new__(typ, *args, **kwargs): cls = super().__new__(typ, *args, **kwargs) if not hasattr(cls, "__reverse_lookup__"): - cls.__reverse_lookup__ = { - v.value: v for v in cls.__members__.values() - } + cls.__reverse_lookup__ = {v.value: v for v in cls.__members__.values()} if not hasattr(cls, "__values__"): cls.__values__ = tuple(v.value for v in cls.__members__.values()) return cls - def from_str(cls: Type[T], value: str) -> T: + def from_str(cls: type[T], value: str) -> T: return cls.__reverse_lookup__[value] # type: ignore[attr-defined] - def values(cls: Type[T]) -> Sequence[str]: + def values(cls: type[T]) -> Sequence[str]: return cls.__values__ # type: ignore[attr-defined] @@ -155,17 +165,21 @@ def is_numeric(cls, datatype): """ Return true if this datatype accepts numerical values """ - if datatype in [VSSDataType.STRING, VSSDataType.STRING_ARRAY, - VSSDataType.BOOLEAN, VSSDataType.BOOLEAN_ARRAY]: + if datatype in [ + VSSDataType.STRING, + VSSDataType.STRING_ARRAY, + VSSDataType.BOOLEAN, + VSSDataType.BOOLEAN_ARRAY, + ]: return False return True -class VSSUnitCollection(): - units: Dict[str, VSSUnit] = dict() +class VSSUnitCollection: + units: dict[str, VSSUnit] = dict() @staticmethod - def get_config_dict(yaml_file: TextIO, key: str) -> Dict[str, Dict[str, str]]: + def get_config_dict(yaml_file: TextIO, key: str) -> dict[str, dict[str, str]]: yaml_config = yaml.safe_load(yaml_file) if (len(yaml_config) == 1) and (key in yaml_config): # Old style unit file @@ -183,7 +197,7 @@ def reset_units(cls): def load_config_file(cls, config_file: str) -> int: added_configs = 0 with open(config_file) as my_yaml_file: - my_units = cls.get_config_dict(my_yaml_file, 'units') + my_units = cls.get_config_dict(my_yaml_file, "units") added_configs = len(my_units) for k, v in my_units.items(): unit = k @@ -209,10 +223,13 @@ def load_config_file(cls, config_file: str) -> int: logging.error("No quantity (domain) found for unit %s", k) sys.exit(-1) - if ((VSSQuantityCollection.nbr_quantities() > 0) and - (VSSQuantityCollection.get_quantity(quantity) is None)): + if (VSSQuantityCollection.nbr_quantities() > 0) and ( + VSSQuantityCollection.get_quantity(quantity) is None + ): # Only give info on first occurance and only if quantities exist at all - logging.info("Quantity %s used by unit %s has not been defined", quantity, k) + logging.info( + "Quantity %s used by unit %s has not been defined", quantity, k + ) VSSQuantityCollection.add_quantity(quantity) allowed_datatypes = None @@ -227,7 +244,9 @@ def load_config_file(cls, config_file: str) -> int: try: VSSDataType.from_str(datatype) except KeyError: - logging.error("Unknown datatype %s in unit definition", datatype) + logging.error( + "Unknown datatype %s in unit definition", datatype + ) sys.exit(-1) unit_node = VSSUnit(k, unit, definition, quantity, allowed_datatypes) @@ -237,16 +256,15 @@ def load_config_file(cls, config_file: str) -> int: return added_configs @classmethod - def get_unit(cls, id: str) -> Optional[VSSUnit]: + def get_unit(cls, id: str) -> VSSUnit | None: if id in cls.units: return cls.units[id] else: return None -class VSSQuantityCollection(): - - quantities: Dict[str, VSSQuantity] = dict() +class VSSQuantityCollection: + quantities: dict[str, VSSQuantity] = dict() @classmethod def reset_quantities(cls): @@ -278,7 +296,7 @@ def load_config_file(cls, config_file: str) -> int: return added_quantities @classmethod - def get_quantity(cls, id: str) -> Optional[VSSQuantity]: + def get_quantity(cls, id: str) -> VSSQuantity | None: if id in cls.quantities: return cls.quantities[id] else: @@ -301,8 +319,8 @@ class VSSTreeType(Enum, metaclass=EnumMetaWithReverseLookup): def available_types(self): if self.value == "signal_tree": - available_types = set(["branch", "sensor", "actuator", "attribute"]) + available_types = {"branch", "sensor", "actuator", "attribute"} else: - available_types = set(["branch", "struct", "property"]) + available_types = {"branch", "struct", "property"} return available_types diff --git a/vspec/model/vsstree.py b/vspec/model/vsstree.py index 5e3a383e..8837d710 100644 --- a/vspec/model/vsstree.py +++ b/vspec/model/vsstree.py @@ -8,18 +8,28 @@ # # SPDX-License-Identifier: MPL-2.0 -from anytree import Node, Resolver, ChildResolverError, RenderTree # type: ignore[import] -from .constants import VSSType, VSSDataType, VSSUnitCollection, VSSUnit -from .exceptions import NameStyleValidationException, \ - ImpossibleMergeException, IncompleteElementException -from typing import Any, Optional, Set, List import copy +import logging import re import sys -import logging +from typing import Any, List, Optional, Set + +from anytree import ( # type: ignore[import] + ChildResolverError, + Node, + RenderTree, + Resolver, +) + +from .constants import VSSDataType, VSSType, VSSUnit, VSSUnitCollection +from .exceptions import ( + ImpossibleMergeException, + IncompleteElementException, + NameStyleValidationException, +) DEFAULT_SEPARATOR = "." -ARRAY_SUBSCRIPT_OP = '[]' +ARRAY_SUBSCRIPT_OP = "[]" class VSSNode(Node): @@ -38,9 +48,27 @@ class VSSNode(Node): # The node types that the nodes can take available_types: Set[str] = set() - core_attributes = ["type", "children", "datatype", "description", "unit", "uuid", "min", "max", "allowed", - "instantiate", "aggregate", "default", "instances", "deprecation", "arraysize", - "comment", "$file_name$", "fka"] + core_attributes = [ + "type", + "children", + "datatype", + "description", + "unit", + "uuid", + "min", + "max", + "allowed", + "instantiate", + "aggregate", + "default", + "instances", + "deprecation", + "arraysize", + "comment", + "$file_name$", + "fka", + "delete", + ] # List of accepted extended attributes. In strict terminate if an attribute is # neither in core or extended, @@ -61,44 +89,61 @@ class VSSNode(Node): expanded = False deprecation = "" fka = "" + delete: bool = False def __deepcopy__(self, memo): # Deep copy of source_dict and children needed as overlay or programmatic changes # in exporters otherwise risk changing values not only for current instances but also for others - return VSSNode(self.name, copy.deepcopy(self.source_dict), self.available_types.copy(), - parent=None, children=copy.deepcopy(self.children, memo)) - - def __init__(self, name, source_dict: dict, available_types: Set[str], parent=None, - children=None, break_on_unknown_attribute=False, break_on_name_style_violation=False): + return VSSNode( + self.name, + copy.deepcopy(self.source_dict), + self.available_types.copy(), + parent=None, + children=copy.deepcopy(self.children, memo), + ) + + def __init__( + self, + name, + source_dict: dict, + available_types: Set[str], + parent=None, + children=None, + break_on_unknown_attribute=False, + break_on_name_style_violation=False, + ): """Creates an VSS Node object from parsed yaml instance represented as a dict. - Args: - name: Name of this VSS instance. - source_dict: VSS instance represented as dict from yaml parsing. - available_types: Available node types asa string list - parent: Optional parent of this node instance. - children: Optional children instances of this node. - break_on_unknown_attribute: Throw if the node contains attributes not in core VSS specification - break_on_name_style_vioation: Throw if this node's name is not follwing th VSS recommended style + Args: + name: Name of this VSS instance. + source_dict: VSS instance represented as dict from yaml parsing. + available_types: Available node types asa string list + parent: Optional parent of this node instance. + children: Optional children instances of this node. + break_on_unknown_attribute: Throw if the node contains attributes not in core VSS specification + break_on_name_style_vioation: Throw if this node's name is not follwing th VSS recommended style - Returns: - VSSNode object according to the Vehicle Signal Specification. + Returns: + VSSNode object according to the Vehicle Signal Specification. """ super().__init__(name, parent, children) self.available_types = available_types - if (source_dict["type"] not in available_types): + if source_dict["type"] not in available_types: logging.error( - f'Invalid type provided for VSSNode: {source_dict["type"]}. Allowed types are {self.available_types}') + f'Invalid type provided for VSSNode: {source_dict["type"]}. Allowed types are {self.available_types}' + ) sys.exit(-1) self.source_dict = source_dict self.unpack_source_dict() - if (self.is_property() and not self.parent.is_struct()): - logging.error(f"Orphan property detected. {self.name} is not defined under a struct") + if self.is_property() and not self.parent.is_struct(): + logging.error( + f"Orphan property detected. {self.name} is not defined under a struct" + ) sys.exit(-1) try: @@ -119,7 +164,11 @@ def unpack_source_dict(self): del self.extended_attributes["type"] def extractCoreAttribute(name: str): - if name != "children" and name != "type" and name in self.source_dict.keys(): + if ( + name != "children" + and name != "type" + and name in self.source_dict.keys() + ): setattr(self, name, self.source_dict[name]) del self.extended_attributes[name] @@ -134,13 +183,18 @@ def extractCoreAttribute(name: str): if self.has_unit(): if not (self.is_signal() or self.is_property()): - logging.error("Item %s cannot have unit, only allowed for signal and property!", self.name) + logging.error( + "Item %s cannot have unit, only allowed for signal and property!", + self.name, + ) sys.exit(-1) unit = self.source_dict["unit"] self.unit = VSSUnitCollection.get_unit(unit) if self.unit is None: - logging.error(f"Unknown unit {unit} for signal {self.qualified_name()}. Terminating.") + logging.error( + f"Unknown unit {unit} for signal {self.qualified_name()}. Terminating." + ) sys.exit(-1) # self.data_type shall only be set if base type is a primitive (VSSDataType) @@ -149,44 +203,61 @@ def extractCoreAttribute(name: str): self.data_type_str = self.source_dict["datatype"] self.validate_and_set_datatype() else: - logging.error("Item %s cannot have datatype, only allowed for signal and property!", self.name) + logging.error( + "Item %s cannot have datatype, only allowed for signal and property!", + self.name, + ) sys.exit(-1) - elif (self.is_signal() or self.is_property()): + elif self.is_signal() or self.is_property(): raise IncompleteElementException( - (f"Incomplete element {self.name} from {self.source_dict['$file_name$']}: " - f"Elements of type {self.type.value} need to have a datatype declared.")) + f"Incomplete element {self.name} from {self.source_dict['$file_name$']}: " + f"Elements of type {self.type.value} need to have a datatype declared." + ) # Datatype check for unit performed first when we have set the right datatype if self.has_unit(): if not self.has_datatype(): - logging.error("Unit specified for item not using standard datatype: %s", self.name) + logging.error( + "Unit specified for item not using standard datatype: %s", self.name + ) sys.exit(-1) if self.has_instances() and not self.is_branch(): logging.error( - f"Only branches can be instantiated. {self.qualified_name()} is of type {self.type}") + f"Only branches can be instantiated. {self.qualified_name()} is of type {self.type}" + ) sys.exit(-1) def validate_name_style(self, sourcefile): """Checks wether this node is adhering to VSS style conventions. - Throws NameStyleValidationException when deviations are detected. A VSS model violating - this conventions can still be a valid model. + Throws NameStyleValidationException when deviations are detected. A VSS model violating + this conventions can still be a valid model. """ - camel_regexp = re.compile('[A-Z][A-Za-z0-9]*$') - if self.is_signal() and self.datatype == VSSDataType.BOOLEAN and not self.name.startswith("Is"): + camel_regexp = re.compile("[A-Z][A-Za-z0-9]*$") + if ( + self.is_signal() + and self.datatype == VSSDataType.BOOLEAN + and not self.name.startswith("Is") + ): raise NameStyleValidationException( - (f'Boolean node "{self.name}" found in file "{sourcefile}" is not following naming conventions. ', - 'It is recommended that boolean nodes start with "Is".')) + ( + f'Boolean node "{self.name}" found in file "{sourcefile}" is not following naming conventions. ', + 'It is recommended that boolean nodes start with "Is".', + ) + ) # relax camel case requirement for struct properties if not self.is_property() and not camel_regexp.match(self.name): raise NameStyleValidationException( - (f'Node "{self.name}" found in file "{sourcefile}" is not following naming conventions. ', - 'It is recommended that node names use camel case, starting with a capital letter, ', - 'only using letters A-z and numbers 0-9.')) + ( + f'Node "{self.name}" found in file "{sourcefile}" is not following naming conventions. ', + "It is recommended that node names use camel case, starting with a capital letter, ", + "only using letters A-z and numbers 0-9.", + ) + ) def base_data_type_str(self) -> str: """ @@ -194,16 +265,16 @@ def base_data_type_str(self) -> str: """ suffix = "[]" if self.data_type_str.endswith(suffix): - return self.data_type_str[:-len(suffix)] + return self.data_type_str[: -len(suffix)] return self.data_type_str def qualified_name(self, separator=DEFAULT_SEPARATOR) -> str: """Returns fully qualified name of a VSS object (including path) using the defined separator (or default ='.') - Args: - separator: Optional parameter as custom separator between path elements of this instance + Args: + separator: Optional parameter as custom separator between path elements of this instance - Returns: - Fully Qualified VSS Node string representation including complete path. + Returns: + Fully Qualified VSS Node string representation including complete path. """ @@ -214,7 +285,7 @@ def qualified_name(self, separator=DEFAULT_SEPARATOR) -> str: path = path.parent node_name = path.name - name = "%s%s%s" % (node_name, separator, name) + name = f"{node_name}{separator}{name}" return name def is_branch(self): @@ -241,8 +312,8 @@ def is_signal(self): def is_orphan(self) -> bool: """Checks if this instance is a branch without any child nodes - Returns: - True if this instance is a branch and has no children. + Returns: + True if this instance is a branch and has no children. """ if self.is_branch() or self.is_struct(): return self.is_leaf @@ -290,64 +361,68 @@ def get_struct_qualified_name(self, struct_name) -> Optional[str]: def is_instantiated(self) -> bool: """Checks if node shall be instantiated through its parent - Returns: - True if it shall be instantiated + Returns: + True if it shall be instantiated """ return self.instantiate def has_unit(self) -> bool: """Checks if this instance has a unit - Returns: - True if this instance has a unit, False otherwise + Returns: + True if this instance has a unit, False otherwise """ return hasattr(self, "unit") and self.unit is not None def get_unit(self) -> str: """Returns: - The name of the unit or empty string if no unit + The name of the unit or empty string if no unit """ if hasattr(self, "unit") and self.unit is not None: return self.unit.value else: - return '' + return "" def has_datatype(self) -> bool: """Check if this instance has a datatype - Returns: - True if this instance has a data type, False otherwise + Returns: + True if this instance has a data type, False otherwise """ return hasattr(self, "datatype") and self.datatype is not None def get_datatype(self) -> str: """Returns: - The name of the datatype or empty string if no datatype + The name of the datatype or empty string if no datatype """ return self.data_type_str def has_instances(self) -> bool: """Check if this instance has a VSS instances - Returns: - True if this instance declares instances, False otherwise + Returns: + True if this instance declares instances, False otherwise """ return hasattr(self, "instances") and self.instances is not None def merge(self, other: "VSSNode"): """Merges two VSSNode, other parameter overwrites the caller object, - if it is not None - Args: - other: other node to merge into the caller object + if it is not None + Args: + other: other node to merge into the caller object """ if self.is_branch() and not other.is_branch(): raise ImpossibleMergeException( - (f"Impossible merging {self.name} from {self.source_dict['$file_name$']} with {other.name} ", - f"from {other.source_dict['$file_name$']}, can not change branch to {other.type.value}.")) + ( + f"Impossible merging {self.name} from {self.source_dict['$file_name$']} with {other.name} ", + f"from {other.source_dict['$file_name$']}, can not change branch to {other.type.value}.", + ) + ) elif not self.is_branch() and other.is_branch(): raise ImpossibleMergeException( - (f"Impossible merging {self.name} from {self.source_dict['$file_name$']} with {other.name} " - f"from {other.source_dict['$file_name$']}, can not change {self.type.value} to branch.")) + f"Impossible merging {self.name} from {self.source_dict['$file_name$']} with {other.name} " + f"from {other.source_dict['$file_name$']}, can not change {self.type.value} to branch." + ) self.source_dict.update(other.source_dict) self.unpack_source_dict() @@ -367,32 +442,47 @@ def validate_and_set_datatype(self): """ is_array = ARRAY_SUBSCRIPT_OP in self.data_type_str # get the base name without subscript decoration - undecorated_datatype_str = self.data_type_str.split( - DEFAULT_SEPARATOR)[-1].replace(ARRAY_SUBSCRIPT_OP, '') + undecorated_datatype_str = self.data_type_str.split(DEFAULT_SEPARATOR)[ + -1 + ].replace(ARRAY_SUBSCRIPT_OP, "") try: self.datatype = VSSDataType.from_str(self.data_type_str) if self.unit and self.unit.allowed_datatypes: - if (not ((undecorated_datatype_str in self.unit.allowed_datatypes) or - (VSSDataType.is_numeric(self.datatype) and "numeric" in self.unit.allowed_datatypes))): - logging.error("Datatype %s not allowed for unit %s", self.data_type_str, self.unit.id) + if not ( + (undecorated_datatype_str in self.unit.allowed_datatypes) + or ( + VSSDataType.is_numeric(self.datatype) + and "numeric" in self.unit.allowed_datatypes + ) + ): + logging.error( + "Datatype %s not allowed for unit %s", + self.data_type_str, + self.unit.id, + ) sys.exit(-1) except KeyError as e: if self.type == VSSType.PROPERTY: # Fully Qualified name as data type name if DEFAULT_SEPARATOR in self.data_type_str: logging.info( - (f"Qualified datatype name {self.data_type_str} provided in node {self.qualified_name()}. ", - "Semantic checks will be performed after the entire tree is rendered. SKIPPING NOW...")) + ( + f"Qualified datatype name {self.data_type_str} provided in node {self.qualified_name()}. ", + "Semantic checks will be performed after the entire tree is rendered. SKIPPING NOW...", + ) + ) else: # Custom data types can contain names defined under the # same branch struct_fqn = self.get_struct_qualified_name( - undecorated_datatype_str) + undecorated_datatype_str + ) if struct_fqn is None: logging.error( - f"Data type not found. Data Type: {undecorated_datatype_str}") + f"Data type not found. Data Type: {undecorated_datatype_str}" + ) sys.exit(-1) # replace data type with qualified name @@ -404,13 +494,16 @@ def validate_and_set_datatype(self): # This is a signal possibly referencing a user-defined type. # Just assign the string value for now. Validation will be # performed after the entire tree is rendered. - logging.debug(f"Possible struct-type encountered - {self.data_type_str} in node {self.name}. ") + logging.debug( + f"Possible struct-type encountered - {self.data_type_str} in node {self.name}. " + ) else: raise e self.datatype = None # reset the enum - def does_attribute_exist(self, other: 'VSSNode', - attr_fn, other_attr_fn, other_filter_fn): + def does_attribute_exist( + self, other: "VSSNode", attr_fn, other_attr_fn, other_filter_fn + ): """ Returns whether the an attribute of this node exists as another attribute in the specified tree @@ -421,15 +514,14 @@ def does_attribute_exist(self, other: 'VSSNode', other_filter_fn: A filter function for node search in the "other" tree. The argument is of type VSSNode. """ key = attr_fn(self) - return key in set(VSSNode.get_tree_attrs( - other, other_attr_fn, other_filter_fn)) + return key in set(VSSNode.get_tree_attrs(other, other_attr_fn, other_filter_fn)) @staticmethod def node_exists(root, node_name) -> bool: """Checks if a node with the name provided to this method exists - Args: - root: root node of tree or root of search if search is applied to subtree - node_name: name of the node that is searched for. Full path (excluding root) is required. + Args: + root: root node of tree or root of search if search is applied to subtree + node_name: name of the node that is searched for. Full path (excluding root) is required. """ try: r = Resolver() @@ -457,18 +549,32 @@ def verify_attributes(self, abort_on_unknown_attribute: bool): unknown = [] for aKey in self.source_dict.keys(): - if aKey not in VSSNode.core_attributes and aKey not in VSSNode.whitelisted_extended_attributes: + if ( + aKey not in VSSNode.core_attributes + and aKey not in VSSNode.whitelisted_extended_attributes + ): unknown.append(aKey) unknown_found = False if len(unknown) > 0: - logging.warning(f"Attribute(s) {', '.join(map(str, unknown))} in element {self.name} not a core " - "or known extended attribute.") + logging.warning( + f"Attribute(s) {', '.join(map(str, unknown))} in element {self.name} not a core " + "or known extended attribute." + ) unknown_found = True if "default" in self.source_dict.keys(): - if self.source_dict["type"] not in {"attribute", "property", "sensor", "actuator"}: - logging.warning("Invalid VSS element %s, %s cannot use default", self.name, self.source_dict["type"]) + if self.source_dict["type"] not in { + "attribute", + "property", + "sensor", + "actuator", + }: + logging.warning( + "Invalid VSS element %s, %s cannot use default", + self.name, + self.source_dict["type"], + ) unknown_found = True if unknown_found and abort_on_unknown_attribute: diff --git a/vspec/vssexporters/vss2ddsidl.py b/vspec/vssexporters/vss2ddsidl.py index 0fdc8360..4a26c5e0 100644 --- a/vspec/vssexporters/vss2ddsidl.py +++ b/vspec/vssexporters/vss2ddsidl.py @@ -289,3 +289,6 @@ def generate(self, config: argparse.Namespace, signal_root: VSSNode, vspec2vss_c with open(config.output_file, 'a' if data_type_root is not None else 'w') as idl_out: export_idl(idl_out, signal_root, vspec2vss_config.generate_uuid, config.all_idl_features) + + def __del__(self): + idlFileBuffer.clear() diff --git a/vspec2binary.py b/vspec2binary.py index 63549707..d2ba8bb4 100755 --- a/vspec2binary.py +++ b/vspec2binary.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2binary import Vss2Binary -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2binary = Vss2Binary(vspec2vss_config) vspec2x = Vspec2X(vss2binary, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2csv.py b/vspec2csv.py index 1cfee950..630b79e9 100755 --- a/vspec2csv.py +++ b/vspec2csv.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2csv import Vss2Csv -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2csv = Vss2Csv() vspec2x = Vspec2X(vss2csv, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2ddsidl.py b/vspec2ddsidl.py index 111aa2cb..0e4435e9 100755 --- a/vspec2ddsidl.py +++ b/vspec2ddsidl.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2ddsidl import Vss2DdsIdl -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2json = Vss2DdsIdl(vspec2vss_config) vspec2x = Vspec2X(vss2json, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2franca.py b/vspec2franca.py index 37f13fa3..e086a87a 100755 --- a/vspec2franca.py +++ b/vspec2franca.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2franca import Vss2Franca -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2franca = Vss2Franca(vspec2vss_config) vspec2x = Vspec2X(vss2franca, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2graphql.py b/vspec2graphql.py index 49e82875..b074450e 100755 --- a/vspec2graphql.py +++ b/vspec2graphql.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2graphql import Vss2Graphql -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2graphql = Vss2Graphql(vspec2vss_config) vspec2x = Vspec2X(vss2graphql, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2json.py b/vspec2json.py index 54b6460d..fa589d8a 100755 --- a/vspec2json.py +++ b/vspec2json.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2json import Vss2Json -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2json = Vss2Json() vspec2x = Vspec2X(vss2json, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2jsonschema.py b/vspec2jsonschema.py index 42bad653..23bd7f68 100755 --- a/vspec2jsonschema.py +++ b/vspec2jsonschema.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2jsonschema import Vss2JsonSchema -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2jsonschema = Vss2JsonSchema() vspec2x = Vspec2X(vss2jsonschema, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2protobuf.py b/vspec2protobuf.py index f7e90e50..1d7a30d7 100755 --- a/vspec2protobuf.py +++ b/vspec2protobuf.py @@ -18,8 +18,12 @@ from vspec.vssexporters.vss2protobuf import Vss2Protobuf -if __name__ == "__main__": +def main(args): vspec2vss_config = Vspec2VssConfig() vss2protobuf = Vss2Protobuf(vspec2vss_config) vspec2x = Vspec2X(vss2protobuf, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/vspec2yaml.py b/vspec2yaml.py index a30ca753..dfe69419 100755 --- a/vspec2yaml.py +++ b/vspec2yaml.py @@ -17,8 +17,13 @@ from vspec.vspec2vss_config import Vspec2VssConfig from vspec.vssexporters.vss2yaml import Vss2Yaml -if __name__ == "__main__": + +def main(args): vspec2vss_config = Vspec2VssConfig() vss2yaml = Vss2Yaml() vspec2x = Vspec2X(vss2yaml, vspec2vss_config) - vspec2x.main(sys.argv[1:]) + vspec2x.main(args) + + +if __name__ == "__main__": + main(sys.argv[1:])