1414import sys
1515from collections .abc import Iterable , Iterator , Sequence
1616from types import ModuleType
17- from typing import Any , Callable , Tuple , Type , Union
17+ from typing import IO , Any , Callable , Tuple , Type , Union
1818
1919
2020class InvalidDataFormat (Exception ):
2121 pass
2222
2323
24+ class InvalidUsage (Exception ):
25+ pass
26+
27+
2428class 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
810869if __name__ == "__main__" :
0 commit comments