Coverage for jsonschema_diff/cli.py: 0%

22 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-15 18:01 +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>>> jsonschema-diff --no-crop-path --all-for-rendering 

15 

16Exit status 

17----------- 

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

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

20 ``--exit-code`` is given) 

21 

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

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

24in sync with the library defaults. 

25 

26""" 

27 

28from __future__ import annotations 

29 

30import argparse 

31import json 

32import sys 

33 

34from jsonschema_diff import ConfigMaker, JsonSchemaDiff 

35from jsonschema_diff.color import HighlighterPipeline 

36from jsonschema_diff.color.stages import ( 

37 MonoLinesHighlighter, 

38 PathHighlighter, 

39 ReplaceGenericHighlighter, 

40) 

41from jsonschema_diff.core.compare_base import Compare 

42 

43 

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

45 """ 

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

47 

48 Parameters 

49 ---------- 

50 disable_color : 

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

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

53 output into a file). 

54 

55 Returns 

56 ------- 

57 HighlighterPipeline 

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

59 three-stage pipeline consisting of 

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

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

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

63 

64 Note 

65 ----- 

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

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

68 self-contained while allowing future customisation. 

69 

70 Examples 

71 -------- 

72 >>> _make_highlighter(True) 

73 HighlighterPipeline(stages=[]) 

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

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

76 """ 

77 if disable_color: 

78 return HighlighterPipeline([]) 

79 return HighlighterPipeline( 

80 [ 

81 MonoLinesHighlighter(), 

82 ReplaceGenericHighlighter(), 

83 PathHighlighter(), 

84 ] 

85 ) 

86 

87 

88def _build_parser() -> argparse.ArgumentParser: 

89 """ 

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

91 

92 Returns 

93 ------- 

94 argparse.ArgumentParser 

95 The fully configured parser containing positional arguments 

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

97 optional feature flags. 

98 

99 See Also 

100 -------- 

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

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

103 """ 

104 p = argparse.ArgumentParser( 

105 prog="jsonschema-diff", 

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

107 ) 

108 

109 # Positional arguments 

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

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

112 

113 # Output options 

114 p.add_argument( 

115 "--no-color", 

116 action="store_true", 

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

118 ) 

119 p.add_argument( 

120 "--legend", 

121 action="store_true", 

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

123 ) 

124 

125 p.add_argument( 

126 "--no-crop-path", 

127 action="store_true", 

128 help="Show flat diff", 

129 ) 

130 p.add_argument( 

131 "--all-for-rendering", 

132 action="store_true", 

133 help="Show the entire file, even those places where there are no changes", 

134 ) 

135 

136 # Exit-code control 

137 p.add_argument( 

138 "--exit-code", 

139 action="store_true", 

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

141 ) 

142 

143 return p 

144 

145 

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

147 """ 

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

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

150 

151 Parameters 

152 ---------- 

153 argv : 

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

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

156 behaviour required by *setuptools* console-scripts. 

157 

158 

159 Note 

160 ---- 

161 The function performs four sequential steps: 

162 

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

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

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

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

167 

168 """ 

169 args = _build_parser().parse_args(argv) 

170 

171 # 1. Build the wrapper object 

172 diff = JsonSchemaDiff( 

173 config=ConfigMaker.make( 

174 all_for_rendering=args.all_for_rendering, crop_path=not bool(args.no_crop_path) 

175 ), 

176 colorize_pipeline=_make_highlighter(args.no_color), 

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

178 ) 

179 

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

181 try: 

182 return dict(json.loads(data)) 

183 except json.JSONDecodeError: 

184 return str(data) 

185 

186 # 2. Compare the files 

187 diff.compare( 

188 old_schema=try_load(args.old_schema), 

189 new_schema=try_load(args.new_schema), 

190 ) 

191 

192 # 3. Print the result 

193 print(args.no_crop_path) 

194 diff.print(with_legend=args.legend) 

195 

196 # 4. Optional special exit code 

197 if args.exit_code: 

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

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

200 

201 

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

203 main()