Coverage for jsonschema_diff/cli.py: 0%

20 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2025-08-25 07:00 +0000

1""" 

2jsonschema_diff CLI 

3=================== 

4 

5A tiny command-line front-end around :py:mod:`jsonschema_diff` 

6that highlights semantic differences between two JSON-Schema 

7documents directly in your terminal. 

8 

9Typical usage 

10------------- 

11>>> jsonschema-diff old.schema.json new.schema.json 

12>>> jsonschema-diff --no-color --legend old.json new.json 

13>>> jsonschema-diff --exit-code old.json new.json # useful in CI 

14 

15Exit status 

16----------- 

17* **0** – the two schemas are semantically identical 

18* **1** – at least one difference was detected (only when 

19 ``--exit-code`` is given) 

20 

21The CLI is intentionally minimal: *all* comparison options are taken 

22from :pyclass:`jsonschema_diff.ConfigMaker`, so the behaviour stays 

23in sync with the library defaults. 

24 

25""" 

26 

27from __future__ import annotations 

28 

29import argparse 

30import json 

31import sys 

32 

33from jsonschema_diff import ConfigMaker, JsonSchemaDiff 

34from jsonschema_diff.color import HighlighterPipeline 

35from jsonschema_diff.color.stages import ( 

36 MonoLinesHighlighter, 

37 PathHighlighter, 

38 ReplaceGenericHighlighter, 

39) 

40from jsonschema_diff.core.parameter_base import Compare 

41 

42 

43def _make_highlighter(disable_color: bool) -> HighlighterPipeline: 

44 """ 

45 Create the high-lighting pipeline used to colorise diff output. 

46 

47 Parameters 

48 ---------- 

49 disable_color : 

50 When *True* ANSI escape sequences are suppressed even if the 

51 invoking TTY advertises color support (e.g. when piping the 

52 output into a file). 

53 

54 Returns 

55 ------- 

56 HighlighterPipeline 

57 Either an **empty** pipeline (no colour) or the standard 

58 three-stage pipeline consisting of 

59 :class:`~jsonschema_diff.color.stages.MonoLinesHighlighter`, 

60 :class:`~jsonschema_diff.color.stages.ReplaceGenericHighlighter` 

61 and :class:`~jsonschema_diff.color.stages.PathHighlighter`. 

62 

63 Note 

64 ----- 

65 The composition of the *default* pipeline mirrors what the core 

66 library exposes; duplicating the stages here keeps the CLI fully 

67 self-contained while allowing future customisation. 

68 

69 Examples 

70 -------- 

71 >>> _make_highlighter(True) 

72 HighlighterPipeline(stages=[]) 

73 >>> _make_highlighter(False).stages # doctest: +ELLIPSIS 

74 [<jsonschema_diff.color.stages.MonoLinesHighlighter ...>, ...] 

75 """ 

76 if disable_color: 

77 return HighlighterPipeline([]) 

78 return HighlighterPipeline( 

79 [ 

80 MonoLinesHighlighter(), 

81 ReplaceGenericHighlighter(), 

82 PathHighlighter(), 

83 ] 

84 ) 

85 

86 

87def _build_parser() -> argparse.ArgumentParser: 

88 """ 

89 Construct the :pyclass:`argparse.ArgumentParser` for the CLI. 

90 

91 Returns 

92 ------- 

93 argparse.ArgumentParser 

94 The fully configured parser containing positional arguments 

95 for the *old* and *new* schema paths, together with three 

96 optional feature flags. 

97 

98 See Also 

99 -------- 

100 * :pyfunc:`main` – where the parser is consumed. 

101 * The *argparse* documentation for available formatting options. 

102 """ 

103 p = argparse.ArgumentParser( 

104 prog="jsonschema-diff", 

105 description="Show the difference between two JSON-Schema files", 

106 ) 

107 

108 # Positional arguments 

109 p.add_argument("old_schema", help="Path to the *old* schema") 

110 p.add_argument("new_schema", help="Path to the *new* schema") 

111 

112 # Output options 

113 p.add_argument( 

114 "--no-color", 

115 action="store_true", 

116 help="Disable ANSI colors even if the terminal supports them", 

117 ) 

118 p.add_argument( 

119 "--legend", 

120 action="store_true", 

121 help="Print a legend explaining diff symbols at the end", 

122 ) 

123 

124 # Exit-code control 

125 p.add_argument( 

126 "--exit-code", 

127 action="store_true", 

128 help="Return **1** if differences are detected, otherwise **0**", 

129 ) 

130 

131 return p 

132 

133 

134def main(argv: list[str] | None = None) -> None: # pragma: no cover 

135 """ 

136 CLI entry-point (invoked by ``python -m jsonschema_diff`` or by the 

137 ``jsonschema-diff`` console script). 

138 

139 Parameters 

140 ---------- 

141 argv : 

142 Command-line argument vector **excluding** the executable name. 

143 When *None* (default) ``sys.argv[1:]`` is used – this is the 

144 behaviour required by *setuptools* console-scripts. 

145 

146 

147 Note 

148 ---- 

149 The function performs four sequential steps: 

150 

151 1. Build a :class:`JsonSchemaDiff` instance. 

152 2. Compare the two user-supplied schema files. 

153 3. Print a colourised diff (optionally with a legend). 

154 4. Optionally exit with code 1 if differences are present. 

155 

156 """ 

157 args = _build_parser().parse_args(argv) 

158 

159 # 1. Build the wrapper object 

160 diff = JsonSchemaDiff( 

161 config=ConfigMaker.make(), 

162 colorize_pipeline=_make_highlighter(args.no_color), 

163 legend_ignore=[Compare], # as in the library example 

164 ) 

165 

166 def try_load(data: str) -> dict | str: 

167 try: 

168 return dict(json.loads(data)) 

169 except json.JSONDecodeError: 

170 return str(data) 

171 

172 # 2. Compare the files 

173 diff.compare( 

174 old_schema=try_load(args.old_schema), 

175 new_schema=try_load(args.new_schema), 

176 ) 

177 

178 # 3. Print the result 

179 diff.print( 

180 with_legend=args.legend, 

181 ) 

182 

183 # 4. Optional special exit code 

184 if args.exit_code: 

185 # ``last_compare_list`` is filled during render/print. 

186 sys.exit(1 if diff.last_compare_list else 0) 

187 

188 

189if __name__ == "__main__": # pragma: no cover 

190 main()