Skip to content

Commit f6838e6

Browse files
committed
pretty up error handling
No longer dump a python stacktrace on failure
1 parent fa517e0 commit f6838e6

File tree

3 files changed

+139
-104
lines changed

3 files changed

+139
-104
lines changed

Justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test:
88
uv run pytest -v
99

1010
bats:
11-
bats tests/bats
11+
bats -T tests/bats
1212

1313
test-all: test bats
1414

jinja2cli/cli.py

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@
1414
import sys
1515
from collections.abc import Iterable, Iterator, Sequence
1616
from types import ModuleType
17-
from typing import Any, Callable, Tuple, Type, Union
17+
from typing import IO, Any, Callable, Tuple, Type, Union
1818

1919

2020
class InvalidDataFormat(Exception):
2121
pass
2222

2323

24+
class InvalidUsage(Exception):
25+
pass
26+
27+
2428
class InvalidInputData(Exception):
2529
pass
2630

@@ -171,33 +175,34 @@ def load_xml() -> FormatLoadResult:
171175
return xmltodict.parse, expat.ExpatError, MalformedXML
172176

173177

174-
def load_env() -> FormatLoadResult:
175-
def parse_env(data: str) -> dict:
176-
"""
177-
Parse an envfile format of key=value pairs that are newline separated.
178-
Supports quoted values with escape sequences.
179-
"""
180-
dict_ = {}
181-
for line in data.splitlines():
182-
line = line.lstrip()
183-
# ignore empty or commented lines
184-
if not line or line[:1] == "#":
185-
continue
186-
k, v = line.split("=", 1)
187-
188-
# Handle quoted values
189-
if v and v[0] in ('"', "'"):
190-
quote = v[0]
191-
if len(v) > 1 and v[-1] == quote:
192-
# Remove surrounding quotes
193-
v = v[1:-1]
194-
# Decode escape sequences for double-quoted values
195-
if quote == '"':
196-
v = v.encode().decode("unicode-escape")
178+
def parse_env(data: str) -> dict:
179+
"""
180+
Parse an envfile format of key=value pairs that are newline separated.
181+
Supports quoted values with escape sequences.
182+
"""
183+
dict_ = {}
184+
for line in data.splitlines():
185+
line = line.lstrip()
186+
# ignore empty or commented lines
187+
if not line or line[:1] == "#":
188+
continue
189+
k, v = line.split("=", 1)
190+
191+
# Handle quoted values
192+
if v and v[0] in ('"', "'"):
193+
quote = v[0]
194+
if len(v) > 1 and v[-1] == quote:
195+
# Remove surrounding quotes
196+
v = v[1:-1]
197+
# Decode escape sequences for double-quoted values
198+
if quote == '"':
199+
v = v.encode().decode("unicode-escape")
200+
201+
dict_[k] = v
202+
return dict_
197203

198-
dict_[k] = v
199-
return dict_
200204

205+
def load_env() -> FormatLoadResult:
201206
return parse_env, Exception, MalformedEnv
202207

203208

@@ -488,8 +493,7 @@ def cli(opts: argparse.Namespace, args: Sequence[str]) -> int:
488493
# Check for invalid mixing of stdin and files
489494
has_stdin = any(f in ("-", "") for f in data_files)
490495
if has_stdin and len(data_files) > 1:
491-
sys.stderr.write("ERROR: Cannot mix stdin (-) with file arguments\n")
492-
return 1
496+
raise InvalidUsage("cannot mix stdin (-) with file arguments")
493497

494498
# Load and merge multiple data files
495499
for data_file in data_files:
@@ -553,8 +557,7 @@ def cli(opts: argparse.Namespace, args: Sequence[str]) -> int:
553557
if section in data:
554558
data = data[section]
555559
else:
556-
sys.stderr.write("ERROR: unknown section. Exiting.")
557-
return 1
560+
raise InvalidUsage(f"unknown section: {section}")
558561

559562
deep_merge(data, parse_kv_string(opts.D or []))
560563

@@ -649,7 +652,7 @@ def __call__(
649652
parser.exit(message=f"jinja2-cli v{__version__}\n - Jinja2 v{jinja_version}\n")
650653

651654

652-
def main() -> None:
655+
def run() -> int:
653656
parser = ArgumentParser(usage="%(prog)s [options] <input template> <input data>")
654657
parser.add_argument(
655658
"--version",
@@ -795,7 +798,7 @@ def main() -> None:
795798
if not opts.stream:
796799
if len(args) == 0:
797800
parser.print_help()
798-
sys.exit(1)
801+
return 1
799802

800803
# Without the second argv, assume they maybe want to read from stdin
801804
if len(args) == 1:
@@ -804,7 +807,63 @@ def main() -> None:
804807
if opts.format not in formats and opts.format != "auto":
805808
raise InvalidDataFormat(opts.format)
806809

807-
sys.exit(cli(opts, args))
810+
return cli(opts, args)
811+
812+
813+
# borrowed from https://github.com/python/cpython/blob/3.14/Lib/_colorize.py#L274
814+
def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
815+
def _safe_getenv(k: str, fallback: str | None = None) -> str | None:
816+
"""Exception-safe environment retrieval. See gh-128636."""
817+
try:
818+
return os.environ.get(k, fallback)
819+
except Exception:
820+
return fallback
821+
822+
if file is None:
823+
file = sys.stdout
824+
825+
if not sys.flags.ignore_environment:
826+
if _safe_getenv("PYTHON_COLORS") == "0":
827+
return False
828+
if _safe_getenv("PYTHON_COLORS") == "1":
829+
return True
830+
if _safe_getenv("NO_COLOR"):
831+
return False
832+
if _safe_getenv("FORCE_COLOR"):
833+
return True
834+
if _safe_getenv("TERM") == "dumb":
835+
return False
836+
837+
if not hasattr(file, "fileno"):
838+
return False
839+
840+
if sys.platform == "win32":
841+
try:
842+
import nt
843+
844+
if not nt._supports_virtual_terminal():
845+
return False
846+
except (ImportError, AttributeError):
847+
return False
848+
849+
try:
850+
return os.isatty(file.fileno())
851+
except OSError:
852+
return hasattr(file, "isatty") and file.isatty()
853+
854+
855+
def main() -> None:
856+
try:
857+
raise SystemExit(run())
858+
except KeyboardInterrupt:
859+
raise SystemExit(130)
860+
except Exception as e:
861+
file = sys.stderr
862+
if can_colorize(file=file):
863+
print(f"\x1b[1;35m{type(e).__name__}\x1b[0m: \x1b[35m{e}\x1b[0m", file=file)
864+
else:
865+
print(f"{type(e).__name__}: {e}", file=file)
866+
raise SystemExit(1)
808867

809868

810869
if __name__ == "__main__":

0 commit comments

Comments
 (0)