From 68843de94c1fa67604dafb22cfd09f7592ad56bd Mon Sep 17 00:00:00 2001 From: Sebastian Schleemilch Date: Fri, 24 Jul 2026 13:03:16 +0200 Subject: [PATCH 1/2] feat: includes as property Signed-off-by: Sebastian Schleemilch --- src/vss_tools/main.py | 9 +--- src/vss_tools/vspec.py | 47 +++++++++++++++---- tests/vspec/test_include/A.vspec | 13 +++++ tests/vspec/test_include/B.vspec | 5 ++ tests/vspec/test_include/B2.vspec | 3 ++ .../test_include/{include_b.vspec => C.vspec} | 11 ++--- tests/vspec/test_include/C2.vspec | 3 ++ tests/vspec/test_include/D.vspec | 3 ++ tests/vspec/test_include/expected.json | 38 --------------- tests/vspec/test_include/expected.plantuml | 24 ---------- tests/vspec/test_include/expected.yaml | 27 +++++++++++ tests/vspec/test_include/include.vspec | 6 --- tests/vspec/test_include/include_c.vspec | 6 --- tests/vspec/test_include/test.vspec | 19 -------- tests/vspec/test_include/test_error.vspec | 6 +-- tests/vspec/test_include/test_include.py | 16 +++---- 16 files changed, 109 insertions(+), 127 deletions(-) create mode 100644 tests/vspec/test_include/A.vspec create mode 100644 tests/vspec/test_include/B.vspec create mode 100644 tests/vspec/test_include/B2.vspec rename tests/vspec/test_include/{include_b.vspec => C.vspec} (58%) create mode 100644 tests/vspec/test_include/C2.vspec create mode 100644 tests/vspec/test_include/D.vspec delete mode 100644 tests/vspec/test_include/expected.json delete mode 100644 tests/vspec/test_include/expected.plantuml create mode 100644 tests/vspec/test_include/expected.yaml delete mode 100644 tests/vspec/test_include/include.vspec delete mode 100644 tests/vspec/test_include/include_c.vspec delete mode 100644 tests/vspec/test_include/test.vspec diff --git a/src/vss_tools/main.py b/src/vss_tools/main.py index 419e1868..1d123e90 100644 --- a/src/vss_tools/main.py +++ b/src/vss_tools/main.py @@ -223,14 +223,9 @@ def get_trees( log.critical(e) exit(1) - unique_include_dirs = [] - for include_dir in include_dirs: - if include_dir not in unique_include_dirs: - unique_include_dirs.append(include_dir) - try: - types_root = get_types_root(types, unique_include_dirs) - vspec_data = load_vspec(unique_include_dirs, [vspec] + list(overlays)) + types_root = get_types_root(types, list(include_dirs)) + vspec_data = load_vspec(list(include_dirs), [vspec] + list(overlays)) except (InvalidSpecDuplicatedEntryException, InvalidSpecException) as e: log.critical(e) exit(1) diff --git a/src/vss_tools/vspec.py b/src/vss_tools/vspec.py index 81dda7e1..97ffa497 100644 --- a/src/vss_tools/vspec.py +++ b/src/vss_tools/vspec.py @@ -15,6 +15,7 @@ import yaml from vss_tools import log +from vss_tools.model import NodeType class IncludeStatementException(Exception): @@ -43,7 +44,7 @@ def __init__(self, statement: str, prefix: str | None = None): split = statement.split() if len(split) < 2: raise IncludeStatementException(f"Malformed include statement: {statement}") - self.target = split[1] + self.target = Path(split[1]) self.prefix = prefix if len(split) == 3: if self.prefix is not None: @@ -52,12 +53,19 @@ def __init__(self, statement: str, prefix: str | None = None): self.prefix = split[2] def resolve_path(self, include_dirs: list[Path]) -> Path: - for dir in include_dirs: - path = dir / self.target - if path.exists(): - log.debug(f"'{self.statement}', resolved={path}") - return path - raise IncludeNotFoundException(f"Unable to find include {self.target}. Include dirs: {include_dirs}") + unique_include_dirs = list(dict.fromkeys(include_dirs)) + candidates = [self.target] + + if self.target.suffix == "": + candidates = [self.target.with_suffix(".vspec"), self.target.with_suffix(".yaml")] + + for dir in unique_include_dirs: + for candidate in candidates: + path = dir / candidate + if path.exists(): + log.debug(f"'{self.statement}', resolved={path.absolute()}") + return path + raise IncludeNotFoundException(f"Unable to find include {self.target}. Include dirs: {unique_include_dirs}") def deep_update(base: dict[str, Any], update: dict[str, Any]) -> None: @@ -86,10 +94,33 @@ def __init__( if self.data is None: self.data = {} + self.includes = [] + for key, value in self.data.items(): if not isinstance(value, dict): raise InvalidSpecException(f"{self.source.absolute()}, Invalid key value: {key}={value}") + # only branches can include things + if value.get("type") != NodeType.BRANCH.value: + continue + + includes = value.get("includes", None) + if includes is None: + continue + + if not isinstance(includes, list): + raise InvalidSpecException(f"{self.source.absolute()}, Invalid 'includes' definition (not a list)") + + for include in includes: + if not isinstance(include, str): + raise InvalidSpecException( + f"{self.source.absolute()}, Invalid 'include' definition (not a str): {include}" + ) + + self.includes.append(Include(f"#include {include} {key}", prefix)) + + del value["includes"] + if prefix: tmp_data = {} for k, v in self.data.items(): @@ -99,7 +130,7 @@ def __init__( lines = content.splitlines() include_statements = [line.strip() for line in lines if line.strip().startswith("#include")] - self.includes = [Include(statement, prefix) for statement in include_statements] + self.includes.extend([Include(statement, prefix) for statement in include_statements]) def __str__(self) -> str: return f"{self.__class__.__name__}, src={self.source}, prefix={self.prefix}, includes={len(self.includes)}" diff --git a/tests/vspec/test_include/A.vspec b/tests/vspec/test_include/A.vspec new file mode 100644 index 00000000..9bc1adeb --- /dev/null +++ b/tests/vspec/test_include/A.vspec @@ -0,0 +1,13 @@ +A: + type: branch + description: d + includes: + - B + +#include C A + +A.X: + type: branch + description: d + +#include D.vspec A.X diff --git a/tests/vspec/test_include/B.vspec b/tests/vspec/test_include/B.vspec new file mode 100644 index 00000000..8cb06b1e --- /dev/null +++ b/tests/vspec/test_include/B.vspec @@ -0,0 +1,5 @@ +B: + type: branch + description: d + includes: + - B2.vspec diff --git a/tests/vspec/test_include/B2.vspec b/tests/vspec/test_include/B2.vspec new file mode 100644 index 00000000..79ec38b8 --- /dev/null +++ b/tests/vspec/test_include/B2.vspec @@ -0,0 +1,3 @@ +B2: + type: branch + description: d diff --git a/tests/vspec/test_include/include_b.vspec b/tests/vspec/test_include/C.vspec similarity index 58% rename from tests/vspec/test_include/include_b.vspec rename to tests/vspec/test_include/C.vspec index e6eb12b0..8fd6dfe0 100644 --- a/tests/vspec/test_include/include_b.vspec +++ b/tests/vspec/test_include/C.vspec @@ -1,11 +1,8 @@ -# Should be put under A.B branch -S3: - datatype: float - type: sensor - unit: km - description: A sensor. +C: + type: branch + description: d # Intentionally no newline, # see previous bug in https://github.com/COVESA/vehicle_signal_specification/issues/145 # No prefix specified so contents shall be put in same branch as S3 -#include include_c.vspec +#include C2.vspec diff --git a/tests/vspec/test_include/C2.vspec b/tests/vspec/test_include/C2.vspec new file mode 100644 index 00000000..ca19e587 --- /dev/null +++ b/tests/vspec/test_include/C2.vspec @@ -0,0 +1,3 @@ +C2: + type: branch + description: d diff --git a/tests/vspec/test_include/D.vspec b/tests/vspec/test_include/D.vspec new file mode 100644 index 00000000..5893dd12 --- /dev/null +++ b/tests/vspec/test_include/D.vspec @@ -0,0 +1,3 @@ +D: + type: branch + description: d diff --git a/tests/vspec/test_include/expected.json b/tests/vspec/test_include/expected.json deleted file mode 100644 index de91daf2..00000000 --- a/tests/vspec/test_include/expected.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "A": { - "children": { - "B": { - "children": { - "S3": { - "datatype": "float", - "description": "A sensor.", - "type": "sensor", - "unit": "km" - }, - "S4": { - "datatype": "float", - "description": "A sensor.", - "type": "sensor", - "unit": "km" - } - }, - "description": "Branch A.B.", - "type": "branch" - }, - "S1": { - "datatype": "float", - "description": "A sensor.", - "type": "sensor", - "unit": "km" - }, - "S2": { - "datatype": "float", - "description": "A sensor.", - "type": "sensor", - "unit": "km" - } - }, - "description": "Branch A.", - "type": "branch" - } -} \ No newline at end of file diff --git a/tests/vspec/test_include/expected.plantuml b/tests/vspec/test_include/expected.plantuml deleted file mode 100644 index 07ec81f6..00000000 --- a/tests/vspec/test_include/expected.plantuml +++ /dev/null @@ -1,24 +0,0 @@ -class A { - - ' sensor: A sensor. - ' unit: km - s1 : float - b : PB.B - - ' sensor: A sensor. - ' unit: km - s2 : float -} -' Branch A.B. -package PB { - class B { - - ' sensor: A sensor. - ' unit: km - s3 : float - - ' sensor: A sensor. - ' unit: km - s4 : float - } -} diff --git a/tests/vspec/test_include/expected.yaml b/tests/vspec/test_include/expected.yaml new file mode 100644 index 00000000..077df2ad --- /dev/null +++ b/tests/vspec/test_include/expected.yaml @@ -0,0 +1,27 @@ +A: + type: branch + description: d + +A.X: + type: branch + description: d + +A.X.D: + type: branch + description: d + +A.B: + type: branch + description: d + +A.B.B2: + type: branch + description: d + +A.C: + type: branch + description: d + +A.C2: + type: branch + description: d diff --git a/tests/vspec/test_include/include.vspec b/tests/vspec/test_include/include.vspec deleted file mode 100644 index 035d4e48..00000000 --- a/tests/vspec/test_include/include.vspec +++ /dev/null @@ -1,6 +0,0 @@ -# Should be put under A branch -S2: - datatype: float - type: sensor - unit: km - description: A sensor. diff --git a/tests/vspec/test_include/include_c.vspec b/tests/vspec/test_include/include_c.vspec deleted file mode 100644 index 6f326453..00000000 --- a/tests/vspec/test_include/include_c.vspec +++ /dev/null @@ -1,6 +0,0 @@ -# Should be put under A.B branch -S4: - datatype: float - type: sensor - unit: km - description: A sensor. diff --git a/tests/vspec/test_include/test.vspec b/tests/vspec/test_include/test.vspec deleted file mode 100644 index 5d59eaf4..00000000 --- a/tests/vspec/test_include/test.vspec +++ /dev/null @@ -1,19 +0,0 @@ -# -A: - type: branch - description: Branch A. - -#include include.vspec A - -# Make sure that we can have additional signals after -A.S1: - datatype: float - type: sensor - unit: km - description: A sensor. - -A.B: - type: branch - description: Branch A.B. - -#include include_b.vspec A.B diff --git a/tests/vspec/test_include/test_error.vspec b/tests/vspec/test_include/test_error.vspec index f37055ca..ec8b93d7 100644 --- a/tests/vspec/test_include/test_error.vspec +++ b/tests/vspec/test_include/test_error.vspec @@ -5,10 +5,10 @@ A: # Not specifying prefix, then we will have a dual root problem as contents from file # are added on root level -#include include.vspec +#include B.vspec # Make sure that we can have additional signals after -A.S1: +A.X: datatype: float type: sensor unit: km @@ -20,4 +20,4 @@ A.B: # As "A.BBBBBB" does not exist we shall get a warning # It will be treated as an implicit branch, which likely is not intentional -#include include_b.vspec A.BBBBBB +#include B.vspec A.BBBBBB diff --git a/tests/vspec/test_include/test_include.py b/tests/vspec/test_include/test_include.py index ebe0f459..d7f48a49 100644 --- a/tests/vspec/test_include/test_include.py +++ b/tests/vspec/test_include/test_include.py @@ -11,23 +11,21 @@ from pathlib import Path HERE = Path(__file__).resolve().parent -TEST_UNITS = HERE / ".." / "test_units.yaml" -TEST_QUANT = HERE / ".." / "test_quantities.yaml" def test_include(tmp_path): - spec = HERE / "test.vspec" - output = tmp_path / "out.json" - expected = HERE / "expected.json" - cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --vspec {spec} --output {output}" + spec = HERE / "A.vspec" + output = tmp_path / "out.yaml" + expected = HERE / "expected.yaml" + cmd = f"vspec export yaml --vspec {spec} --output {output}" subprocess.run(cmd.split(), check=True) - filecmp.cmp(output, expected) + assert filecmp.cmp(output, expected) def test_error(tmp_path): spec = HERE / "test_error.vspec" - output = tmp_path / "out.json" - cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --vspec {spec} --output {output}" + output = tmp_path / "out.yaml" + cmd = f"vspec export yaml --vspec {spec} --output {output}" process = subprocess.run(cmd.split(), capture_output=True, text=True) assert process.returncode != 0 From 0ad4ead2277d0a8a4ae1cba3903621b2284b036f Mon Sep 17 00:00:00 2001 From: Sebastian Schleemilch Date: Mon, 27 Jul 2026 13:06:40 +0200 Subject: [PATCH 2/2] feat: also search for .yml when using includes Signed-off-by: Sebastian Schleemilch --- src/vss_tools/vspec.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vss_tools/vspec.py b/src/vss_tools/vspec.py index 97ffa497..e64222ed 100644 --- a/src/vss_tools/vspec.py +++ b/src/vss_tools/vspec.py @@ -57,7 +57,11 @@ def resolve_path(self, include_dirs: list[Path]) -> Path: candidates = [self.target] if self.target.suffix == "": - candidates = [self.target.with_suffix(".vspec"), self.target.with_suffix(".yaml")] + candidates = [ + self.target.with_suffix(".vspec"), + self.target.with_suffix(".yaml"), + self.target.with_suffix(".yml"), + ] for dir in unique_include_dirs: for candidate in candidates: