• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nvidia-holoscan / holoscan-cli / 30079165932

24 Jul 2026 08:30AM UTC coverage: 79.178% (+0.6%) from 78.605%
30079165932

Pull #205

github

wyli
feat: add --json to list, modes, env-info, and version

Completes the F3 "--json on every read verb" surface from the agentic-CLI
design. `status` and `env-check` already emitted JSON; this adds the same
opt-in `--json` flag to the four remaining read commands so agents can parse
discovery and diagnostic output instead of scraping prose:

* `list --json`     — {projects: [{name, project_type, source_folder,
                      language[], modes[]}]}. Includes source_folder, the one
                      field agents previously had to guess.
* `modes --json`    — the resolved metadata.modes object plus project/language.
* `env-info --json` — full structured host report. env_info.py grows a
                      parallel `gather_*` data layer beside the `collect_*`
                      prose printers (the status.py collect/format split);
                      only one path runs per invocation.
* `version --json`  — package/version/executable/module.

All payloads flow through a new utils/json_output.dumps that injects an
additive `schema_version` (1) as the first key; the same wrapper is
retrofitted onto the existing status/env-check JSON (additive, no field moves).
Env-var name lists and the Python-environment classification are extracted to
single-source helpers shared by the prose and JSON renderers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull Request #205: feat: add --json to list, modes, env-info, and version

132 of 136 new or added lines in 7 files covered. (97.06%)

59 existing lines in 4 files now uncovered.

3236 of 4087 relevant lines covered (79.18%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.19
/src/holoscan_cli/metadata/utils.py
1
# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
#
4
# Licensed under the Apache License, Version 2.0 (the "License");
5
# you may not use this file except in compliance with the License.
6
# You may obtain a copy of the License at
7
#
8
# http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
# Utility helpers shared across metadata consumers.
16

17
import os
1✔
18
from collections.abc import Iterable, Iterator, Sequence
1✔
19
from pathlib import Path
1✔
20

21
METADATA_DIRECTORY_CONFIG = {
1✔
22
    "applications": {
23
        "ignore_patterns": ["template"],
24
        "metadata_is_required": True,
25
        "schema": "application",
26
    },
27
    "modules": {
28
        "ignore_patterns": ["template"],
29
        "metadata_is_required": True,
30
        "schema": "module",
31
    },
32
    "benchmarks": {"schema": "benchmark"},
33
    "gxf_extensions": {
34
        "ignore_patterns": ["utils"],
35
        "metadata_is_required": True,
36
        "schema": "gxf_extension",
37
    },
38
    "operators": {
39
        "ignore_patterns": ["template"],
40
        "metadata_is_required": True,
41
        "schema": "operator",
42
    },
43
    "pkg": {"metadata_is_required": False, "schema": "package"},
44
    "tutorials": {
45
        "ignore_patterns": ["template"],
46
        "metadata_is_required": False,
47
        "schema": "tutorial",
48
    },
49
    "workflows": {
50
        "ignore_patterns": ["template"],
51
        "metadata_is_required": True,
52
        "schema": "workflow",
53
    },
54
}
55

56
SCHEMA_DIR = Path(__file__).resolve().parent
1✔
57
BASE_SCHEMA_PATH = SCHEMA_DIR / "project.schema.json"
1✔
58

59
DEFAULT_INCLUDE_PATHS = tuple(METADATA_DIRECTORY_CONFIG.keys())
1✔
60

61

62
def get_schema_path(directory: str | os.PathLike) -> Path:
1✔
63
    """Return the schema file path for a given project directory name."""
64
    dir_name = Path(directory).name
1✔
65
    config = METADATA_DIRECTORY_CONFIG.get(dir_name, {})
1✔
66
    schema_name = config.get("schema", dir_name)
1✔
67
    return SCHEMA_DIR / f"{schema_name}.schema.json"
1✔
68

69

70
def normalize_language(language: str | None, *, strict: bool = False) -> str:
1✔
71
    """Normalize language names, optionally enforcing known HoloHub languages."""
72
    if not language or not isinstance(language, str):
1✔
73
        return ""
1✔
74
    lang = language.strip().lower()
1✔
75
    if lang in ("cpp", "c++"):
1✔
UNCOV
76
        normalized = "cpp"
×
77
    elif lang in ("python", "py"):
1✔
78
        normalized = "python"
1✔
79
    else:
UNCOV
80
        normalized = lang
×
81

82
    if strict and normalized not in ("", "cpp", "python"):
1✔
UNCOV
83
        raise ValueError(f"Invalid language: {language}")
×
84
    return normalized
1✔
85

86

87
def list_normalized_languages(language, *, strict: bool = False) -> list[str]:
1✔
88
    """Return a list of normalized language tags from a single value or sequence."""
89
    if isinstance(language, str) or language is None:
1✔
90
        values = [language]
1✔
UNCOV
91
    elif isinstance(language, Iterable):
×
UNCOV
92
        values = list(language)
×
93
    else:
UNCOV
94
        values = []
×
95

96
    normalized = [
1✔
97
        normalize_language(value, strict=strict)
98
        for value in values
99
        if value is None or isinstance(value, str)
100
    ]
101
    normalized = [value for value in normalized if value]
1✔
102
    return normalized or [""]
1✔
103

104

105
def iter_metadata_paths(
1✔
106
    repo_paths: Sequence[str | os.PathLike],
107
    *,
108
    exclude_patterns: Sequence[str] | None = None,
109
) -> Iterator[str]:
110
    """Yield filtered metadata.json paths."""
111
    excludes = [pattern for pattern in (exclude_patterns or []) if pattern]
1✔
112

113
    def _matches_segment(path: str, patterns: Sequence[str]) -> bool:
1✔
114
        # Pattern must appear as a complete (slash-bordered) path segment, so
115
        # ignore_pattern "template" matches "applications/template/foo" but
116
        # not "holoscan-module-template/foo". Multi-segment patterns like
117
        # "applications/holoviz/template" still work.
118
        norm = "/" + path.replace(os.sep, "/").strip("/") + "/"
1✔
119
        return any(
1✔
120
            ("/" + pattern.replace(os.sep, "/").strip("/") + "/") in norm for pattern in patterns
121
        )
122

123
    for repo_path in repo_paths:
1✔
124
        for root, _, files in os.walk(repo_path):
1✔
125
            if "metadata.json" not in files:
1✔
126
                continue
1✔
127
            file_path = os.path.join(root, "metadata.json")
1✔
128
            if excludes and _matches_segment(file_path, excludes):
1✔
UNCOV
129
                continue
×
130

131
            directory_config: dict = {}
1✔
132
            for part in Path(file_path).parts:
1✔
133
                if part in METADATA_DIRECTORY_CONFIG:
1✔
134
                    directory_config = METADATA_DIRECTORY_CONFIG[part]
1✔
135
                    break
1✔
136

137
            ignore_patterns = directory_config.get("ignore_patterns", [])
1✔
138
            if ignore_patterns and _matches_segment(file_path, ignore_patterns):
1✔
UNCOV
139
                continue
×
140

141
            yield file_path
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc