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

nvidia-holoscan / holoscan-cli / 30080916025

24 Jul 2026 08:59AM UTC coverage: 79.08% (+0.5%) from 78.605%
30080916025

Pull #205

github

web-flow
Merge b527f29f9 into 6c8cf985c
Pull Request #205: feat: add --json to list, modes, env-info, and version

134 of 140 new or added lines in 9 files covered. (95.71%)

5 existing lines in 2 files now uncovered.

3232 of 4087 relevant lines covered (79.08%)

0.79 hits per line

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

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

17
"""Pure data helpers — text/version/distance, env/arg parsing, filesystem stats.
18

19
No subprocess and no terminal I/O — everything in this module is testable
20
without side effects beyond reading os.environ or the filesystem.
21
"""
22

23
import os
1✔
24
import re
1✔
25
import time
1✔
26
from pathlib import Path
1✔
27
from typing import List, Optional, Tuple
1✔
28

29
# ---- version parsing ---------------------------------------------------------
30

31

32
def parse_semantic_version(version: str) -> Tuple[int, int, int]:
1✔
33
    """
34
    Parse semantic version string MAJOR.MINOR.PATCH into tuple of integers for comparison
35

36
    Note: Implementing our own version parsing to avoid dependency on PyPI 'packaging' module.
37

38
    ref: https://semver.org/
39
    """
40
    match = re.match(r"^(\d+\.\d+\.\d+).*", version.strip())
1✔
41
    if not match:
1✔
42
        raise ValueError(f"Failed to parse semantic version string: {version}")
×
43
    return tuple(map(int, match.group(1).split(".")))
1✔
44

45

46
# ---- string helpers ----------------------------------------------------------
47

48

49
def _slugify(text: str, max_len: int = 63) -> str:
1✔
50
    """Make a branch slug: lowercase, non-alnum to '-', trim dashes, max length."""
UNCOV
51
    lowered = text.lower()
×
UNCOV
52
    replaced = re.sub(r"[^a-z0-9]+", "-", lowered)
×
UNCOV
53
    trimmed = replaced.strip("-")
×
UNCOV
54
    return trimmed[:max_len]
×
55

56

57
def levenshtein_distance(s1: str, s2: str) -> int:
1✔
58
    """Calculate the Levenshtein distance between two strings."""
59
    s1 = s1.lower()
1✔
60
    s2 = s2.lower()
1✔
61

62
    if len(s1) < len(s2):
1✔
63
        return levenshtein_distance(s2, s1)
×
64

65
    if len(s2) == 0:
1✔
66
        return len(s1)
×
67

68
    previous_row = range(len(s2) + 1)
1✔
69
    for i, c1 in enumerate(s1):
1✔
70
        current_row = [i + 1]
1✔
71
        for j, c2 in enumerate(s2):
1✔
72
            insertions = previous_row[j + 1] + 1
1✔
73
            deletions = current_row[j] + 1
1✔
74
            substitutions = previous_row[j] + (c1 != c2)
1✔
75
            current_row.append(min(insertions, deletions, substitutions))
1✔
76
        previous_row = current_row
1✔
77

78
    return previous_row[-1]
1✔
79

80

81
# ---- env / CLI arg helpers ---------------------------------------------------
82

83

84
_FALSE_ENV_VALUES: Tuple[str, ...] = ("false", "no", "n", "0", "f", "off")
1✔
85

86

87
def is_env_flag_true(value: Optional[str]) -> bool:
1✔
88
    """Return whether an optional environment flag is enabled."""
89
    normalized = (value or "").strip().lower()
1✔
90
    return bool(normalized) and normalized not in _FALSE_ENV_VALUES
1✔
91

92

93
def get_env_bool(
1✔
94
    env_var_name: str,
95
    default: bool = True,
96
    false_values: Tuple[str, ...] = _FALSE_ENV_VALUES,
97
) -> Tuple[str, bool]:
98
    """Check environment variable as boolean flag"""
99
    env_value = os.environ.get(env_var_name, str(default).lower())
1✔
100
    is_true = env_value.lower() not in false_values
1✔
101
    return env_value, is_true
1✔
102

103

104
def get_cli_arg_value(args: List[str], flag: str) -> Optional[str]:
1✔
105
    """Return the last value of ``flag`` in a CLI argument list.
106

107
    Supports both ``--flag value`` and ``--flag=value`` forms. Returns ``None``
108
    if the flag is not present. The last-wins rule mirrors typical CLI behavior
109
    where later occurrences override earlier ones.
110
    """
111
    value: Optional[str] = None
1✔
112
    prefix = f"{flag}="
1✔
113
    i = 0
1✔
114
    while i < len(args):
1✔
115
        arg = args[i]
1✔
116
        if arg == flag and i + 1 < len(args):
1✔
117
            value = args[i + 1]
1✔
118
            i += 2
1✔
119
            continue
1✔
120
        if arg.startswith(prefix):
1✔
121
            value = arg.removeprefix(prefix)
1✔
122
        i += 1
1✔
123
    return value
1✔
124

125

126
def normalize_args_str(args):
1✔
127
    """Convert arguments to string format, handling both string and array inputs"""
128
    if isinstance(args, str):
1✔
129
        return os.path.expandvars(args)
1✔
130
    elif isinstance(args, list):
1✔
131
        expanded_args = [os.path.expandvars(arg) for arg in args]
1✔
132
        return " ".join(expanded_args)
1✔
133
    return ""
×
134

135

136
# ---- filesystem stats + reporting --------------------------------------------
137

138

139
def dir_size_mb(path: Path) -> float:
1✔
140
    """Return the total size of a directory tree in megabytes."""
141
    total = 0
1✔
142
    for root, _dirs, files in os.walk(str(path)):
1✔
143
        for f in files:
1✔
144
            try:
1✔
145
                total += os.path.getsize(os.path.join(root, f))
1✔
146
            except OSError:
×
147
                continue
×
148
    return total / (1024 * 1024)
1✔
149

150

151
def relative_time(mtime: float) -> str:
1✔
152
    """Format an mtime as a human-readable relative time string."""
153
    elapsed = time.time() - mtime
1✔
154
    if elapsed < 60:
1✔
155
        return "just now"
1✔
156
    if elapsed < 3600:
×
157
        return f"{int(elapsed / 60)}m ago"
×
158
    if elapsed < 86400:
×
159
        return f"{int(elapsed / 3600)}h ago"
×
160
    return f"{int(elapsed / 86400)}d ago"
×
161

162

163
def format_size(mb: float) -> str:
1✔
164
    """Format a size in megabytes as a human-readable string."""
165
    if mb >= 1024:
1✔
166
        return f"{mb / 1024:.1f} GB"
1✔
167
    return f"{mb:.0f} MB"
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