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

thesimj / tomlev / 18126767925

30 Sep 2025 10:25AM UTC coverage: 91.42% (-1.5%) from 92.881%
18126767925

push

github

Nick Bubelich
Add caching for file reading, improve substitution logic, and enhance error handling.

- Introduced `_read_file_cached` with `lru_cache

41 of 51 new or added lines in 4 files covered. (80.39%)

4 existing lines in 1 file now uncovered.

586 of 641 relevant lines covered (91.42%)

0.91 hits per line

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

84.09
/tomlev/cli.py
1
"""
2
MIT License
3

4
Copyright (c) 2025 Nick Bubelich
5

6
Permission is hereby granted, free of charge, to any person obtaining a copy
7
of this software and associated documentation files (the "Software"), to deal
8
in the Software without restriction, including without limitation the rights
9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
copies of the Software, and to permit persons to whom the Software is
11
furnished to do so, subject to the following conditions:
12

13
The above copyright notice and this permission notice shall be included in all
14
copies or substantial portions of the Software.
15

16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
SOFTWARE.
23
"""
24

25
from __future__ import annotations
1✔
26

27
import argparse
1✔
28
import json
1✔
29
from os import environ
1✔
30

31
from .constants import (
1✔
32
    DEFAULT_ENV_FILE,
33
    DEFAULT_SEPARATOR,
34
    DEFAULT_TOML_FILE,
35
    TOMLEV_ENV_FILE,
36
    TOMLEV_TOML_FILE,
37
    VERSION,
38
)
39
from .env_loader import EnvDict, read_env_file
1✔
40
from .errors import ConfigValidationError
1✔
41
from .parser import read_toml
1✔
42

43
__all__ = ["cli_validate", "cli_render", "main"]
1✔
44

45

46
def cli_validate(toml_file: str, env_file: str | None, strict: bool, include_environment: bool, separator: str) -> int:
1✔
47
    """Validate TOML parsing and env substitution without a model.
48

49
    Returns exit code 0 on success, 1 on failure. Prints a concise
50
    success message or a meaningful error to stderr.
51

52
    Args:
53
        toml_file: Path to the TOML file to validate.
54
        env_file: Path to the .env file, or None to skip.
55
        strict: Whether to operate in strict mode.
56
        include_environment: Whether to include system environment variables.
57
        separator: Separator for default values in environment variables.
58

59
    Returns:
60
        Exit code: 0 on success, 1 on failure.
61
    """
62
    # Build env mapping
63
    env: EnvDict = dict(environ) if include_environment else {}
1✔
64
    # Read dotenv
65
    try:
1✔
66
        dotenv = read_env_file(env_file, strict)
1✔
NEW
67
    except (OSError, IOError, PermissionError) as e:
×
UNCOV
68
        print(f"Error reading env file: {e}")
×
UNCOV
69
        return 1
×
NEW
70
    except ConfigValidationError as e:
×
NEW
71
        print(str(e))
×
NEW
72
        return 1
×
73
    env.update(dotenv)
1✔
74

75
    # Read TOML and perform substitution
76
    try:
1✔
77
        read_toml(toml_file, env, strict, separator)
1✔
78
    except FileNotFoundError:
1✔
79
        print(f"TOML file not found: {toml_file}")
1✔
80
        return 1
1✔
81
    except ConfigValidationError as e:
1✔
82
        print(str(e))
1✔
83
        return 1
1✔
84
    except (OSError, IOError, ValueError, TypeError) as e:
1✔
85
        print(f"Validation error: {e}")
1✔
86
        return 1
1✔
87

88
    print("Validation successful.")
1✔
89
    return 0
1✔
90

91

92
def cli_render(toml_file: str, env_file: str | None, strict: bool, include_environment: bool, separator: str) -> int:
1✔
93
    """Render TOML configuration as JSON after environment substitution and includes.
94

95
    Returns exit code 0 on success, 1 on failure. Prints the rendered
96
    configuration as pretty-formatted JSON to stdout.
97

98
    Args:
99
        toml_file: Path to the TOML file to render.
100
        env_file: Path to the .env file, or None to skip.
101
        strict: Whether to operate in strict mode.
102
        include_environment: Whether to include system environment variables.
103
        separator: Separator for default values in environment variables.
104

105
    Returns:
106
        Exit code: 0 on success, 1 on failure.
107
    """
108
    # Build env mapping
109
    env: EnvDict = dict(environ) if include_environment else {}
1✔
110
    # Read dotenv
111
    try:
1✔
112
        dotenv = read_env_file(env_file, strict)
1✔
NEW
113
    except (OSError, IOError, PermissionError) as e:
×
UNCOV
114
        print(f"Error reading env file: {e}")
×
UNCOV
115
        return 1
×
NEW
116
    except ConfigValidationError as e:
×
NEW
117
        print(str(e))
×
NEW
118
        return 1
×
119
    env.update(dotenv)
1✔
120

121
    # Read TOML and perform substitution
122
    try:
1✔
123
        config = read_toml(toml_file, env, strict, separator)
1✔
124
    except FileNotFoundError:
1✔
125
        print(f"TOML file not found: {toml_file}")
1✔
126
        return 1
1✔
127
    except ConfigValidationError as e:
1✔
128
        print(str(e))
×
129
        return 1
×
130
    except (OSError, IOError, ValueError, TypeError) as e:
1✔
131
        print(f"Render error: {e}")
1✔
132
        return 1
1✔
133

134
    # Output as JSON
135
    print(json.dumps(config, indent=2))
1✔
136
    return 0
1✔
137

138

139
def main(argv: list[str] | None = None) -> int:
1✔
140
    """TomlEv CLI entry point.
141

142
    Commands:
143
    - validate: Validate TOML file with env substitution (schema-less).
144
    - render: Render TOML configuration as JSON with substitution and includes.
145

146
    Args:
147
        argv: Command line arguments. If None, uses sys.argv.
148

149
    Returns:
150
        Exit code: 0 on success, 1 on failure.
151
    """
152
    # Get defaults from environment variables or fall back to constants
153
    default_toml = environ.get(TOMLEV_TOML_FILE, DEFAULT_TOML_FILE)
1✔
154
    default_env = environ.get(TOMLEV_ENV_FILE, DEFAULT_ENV_FILE)
1✔
155

156
    parser = argparse.ArgumentParser(prog="tomlev", description="TomlEv CLI")
1✔
157
    parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {VERSION}")
1✔
158
    sub = parser.add_subparsers(dest="command", required=True)
1✔
159

160
    p_validate = sub.add_parser("validate", help="Validate TOML with env substitution")
1✔
161
    p_validate.add_argument("--toml", default=default_toml, help="Path to TOML file")
1✔
162
    p_validate.add_argument("--env-file", default=default_env, help="Path to .env file (use --no-env-file to disable)")
1✔
163
    p_validate.add_argument("--no-env-file", action="store_true", help="Do not read .env file")
1✔
164
    p_validate.add_argument(
1✔
165
        "--strict", dest="strict", action="store_true", default=True, help="Enable strict mode (default)"
166
    )
167
    p_validate.add_argument("--no-strict", dest="strict", action="store_false", help="Disable strict mode")
1✔
168
    p_validate.add_argument("--no-environ", action="store_true", help="Do not include system environment variables")
1✔
169
    p_validate.add_argument("--separator", default=DEFAULT_SEPARATOR, help="Default separator for ${VAR|-default}")
1✔
170

171
    p_render = sub.add_parser("render", help="Render TOML configuration as JSON")
1✔
172
    p_render.add_argument("--toml", default=default_toml, help="Path to TOML file")
1✔
173
    p_render.add_argument("--env-file", default=default_env, help="Path to .env file (use --no-env-file to disable)")
1✔
174
    p_render.add_argument("--no-env-file", action="store_true", help="Do not read .env file")
1✔
175
    p_render.add_argument(
1✔
176
        "--strict", dest="strict", action="store_true", default=True, help="Enable strict mode (default)"
177
    )
178
    p_render.add_argument("--no-strict", dest="strict", action="store_false", help="Disable strict mode")
1✔
179
    p_render.add_argument("--no-environ", action="store_true", help="Do not include system environment variables")
1✔
180
    p_render.add_argument("--separator", default=DEFAULT_SEPARATOR, help="Default separator for ${VAR|-default}")
1✔
181

182
    args = parser.parse_args(argv)
1✔
183

184
    if args.command == "validate":
1✔
185
        env_file = None if args.no_env_file else args.env_file
1✔
186
        include_environment = not args.no_environ
1✔
187
        return cli_validate(args.toml, env_file, args.strict, include_environment, args.separator)
1✔
188
    elif args.command == "render":
1✔
189
        env_file = None if args.no_env_file else args.env_file
1✔
190
        include_environment = not args.no_environ
1✔
191
        return cli_render(args.toml, env_file, args.strict, include_environment, args.separator)
1✔
192

193
    parser.print_help()  # pragma: no cover - subparsers require a command
194
    return 1  # pragma: no cover - unreachable with required=True
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc