Coverage for python/pyshacl_turbo/cli.py: 100.0%

130 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-16 12:25 +0000

1"""Command-line I/O for the native validator and rule runner.""" 

2 

3import argparse 1Ob

4import logging 1Ob

5import sys 1Ob

6from contextlib import ExitStack 1Ob

7from importlib.metadata import version 1Ob

8 

9from rdflib import Dataset, Graph 1Ob

10from rdflib.namespace import SH 1Ob

11 

12from . import shacl_rules, validate, validate_each 1Ob

13 

14 

15def table(headers, rows): 1Ob

16 def split(value): 1Nac

17 text = str(value) 1Nac

18 return [text[i : i + 25] for i in range(0, len(text), 25)] or [""] 1Nac

19 

20 cells = [[split(value) for value in row] for row in [headers, *rows]] 1Nac

21 widths = [max(len(line) for row in cells for line in row[i]) for i in range(len(headers))] 1Nac

22 border = "+" + "+".join("-" * (width + 2) for width in widths) + "+" 1Nac

23 result = [border] 1Nac

24 for index, row in enumerate(cells): 1Nac

25 for line in range(max(map(len, row))): 1Nac

26 result.append( 1Nac

27 "| " 

28 + " | ".join( 

29 (column[line] if line < len(column) else "").ljust(width) 

30 for column, width in zip(row, widths) 

31 ) 

32 + " |" 

33 ) 

34 if index == 0: 1Nac

35 result.append(border) 1Nac

36 result.append(border) 1Nac

37 return "\n".join(result) + "\n" 1Nac

38 

39 

40def report_table(conforms, report): 1Ob

41 output = table(["Conforms"], [[conforms]]) 1Nac

42 if conforms: 1Nac

43 return output 1c

44 rows = [] 1Na

45 fields = [ 1Na

46 SH.resultSeverity, 

47 SH.focusNode, 

48 SH.resultPath, 

49 SH.resultMessage, 

50 SH.sourceConstraintComponent, 

51 SH.sourceShape, 

52 SH.value, 

53 ] 

54 for index, result in enumerate(report.objects(None, SH.result), 1): 1Na

55 values = [ 1a

56 str(report.value(result, predicate) or "-").replace(str(SH), "") for predicate in fields 

57 ] 

58 rows.extend([[index, *values], [""] * 8]) 1a

59 return ( 1Na

60 output 

61 + "\n" 

62 + table( 

63 [ 

64 "No.", 

65 "Severity", 

66 "Focus Node", 

67 "Result Path", 

68 "Message", 

69 "Component", 

70 "Shape", 

71 "Value", 

72 ], 

73 rows, 

74 ) 

75 ) 

76 

77 

78def serialize_graph(graph, rdf_format): 1Ob

79 if rdf_format in {"trig", "nquads"} and not graph.context_aware: 1fgCDEhjkdlemnFopqrsitu

80 dataset = Dataset() 1ghde

81 dataset.default_graph += graph 1ghde

82 for prefix, namespace in graph.namespaces(): 1ghde

83 dataset.bind(prefix, namespace) 1ghde

84 graph = dataset 1ghde

85 return graph.serialize(format=rdf_format) 1fgCDEhjkdlemnFopqrsitu

86 

87 

88def main(argv=None): 1Ob

89 parser = argparse.ArgumentParser(prog="pyshacl-turbo") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

90 parser.add_argument("data", nargs="*") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

91 parser.add_argument("-s", "--shapes", "--shacl", dest="shacl_graph") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

92 parser.add_argument("-e", "--ont-graph", dest="ont_graph") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

93 parser.add_argument( 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

94 "-i", "--inference", choices=["none", "rdfs", "owlrl", "both"], default="none" 

95 ) 

96 parser.add_argument("-a", "--advanced", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

97 parser.add_argument("-m", "--metashacl", dest="meta_shacl", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

98 parser.add_argument("-j", "--js", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

99 parser.add_argument("-im", "--imports", dest="do_owl_imports", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

100 parser.add_argument("-it", "--iterate-rules", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

101 parser.add_argument("--abort", dest="abort_on_first", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

102 parser.add_argument("--allow-info", "--allow-infos", dest="allow_infos", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

103 parser.add_argument( 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

104 "-w", "--allow-warning", "--allow-warnings", dest="allow_warnings", action="store_true" 

105 ) 

106 parser.add_argument("--max-depth", dest="max_validation_depth", type=int) 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

107 parser.add_argument("--validate-each", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

108 parser.add_argument("--focus") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

109 parser.add_argument("--shape") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

110 parser.add_argument("-d", "--debug", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

111 parser.add_argument("--rules", action="store_true") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

112 parser.add_argument("-df", "--data-file-format", dest="data_graph_format") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

113 parser.add_argument("-sf", "--shacl-file-format", dest="shacl_graph_format") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

114 parser.add_argument("-ef", "--ont-file-format", dest="ont_graph_format") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

115 parser.add_argument( 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

116 "-f", 

117 "--format", 

118 default="human", 

119 choices=["human", "table", "turtle", "trig", "xml", "json-ld", "nt", "n3", "nquads"], 

120 ) 

121 parser.add_argument("-o", "--output") 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

122 parser.add_argument("-V", "--version", action="version", version=version("pyshacl-turbo")) 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

123 args = parser.parse_args(argv) 1xGHJyzAfIbvgwCDEhjkdlemnacLKFMBopqrsitu

124 if not args.data: 1xGHJyzAfIbvgwCDEhjkdlemnacLKFBopqrsitu

125 print("Input Error. No DataGraph file or endpoint supplied.", file=sys.stderr) 1L

126 parser.print_usage(sys.stderr) 1L

127 return 1 1L

128 if args.debug: 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

129 logging.basicConfig(level=logging.DEBUG) 1f

130 options = { 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

131 name: getattr(args, name) 

132 for name in [ 

133 "shacl_graph", 

134 "ont_graph", 

135 "inference", 

136 "advanced", 

137 "js", 

138 "do_owl_imports", 

139 "iterate_rules", 

140 "data_graph_format", 

141 "shacl_graph_format", 

142 "ont_graph_format", 

143 "debug", 

144 "max_validation_depth", 

145 ] 

146 } 

147 for name in ("data_graph_format", "shacl_graph_format", "ont_graph_format"): 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

148 if options[name] == "auto": 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

149 options[name] = None 1i

150 options.update( 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

151 focus_nodes=[name.strip() for name in args.focus.split(",")] if args.focus else None, 

152 use_shapes=[name.strip() for name in args.shape.split(",")] if args.shape else None, 

153 ) 

154 with ExitStack() as files: 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

155 try: 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

156 data = [ 1xGHJyzAfIbvgwCDEhjkdlemnacKFBopqrsitu

157 sys.stdin.buffer.read() 

158 if source == "-" 

159 else files.enter_context(open(source, "rb")) 

160 for source in args.data 

161 ] 

162 except FileNotFoundError: 1JIK

163 print("Input Error. DataGraph file not found.", file=sys.stderr) 1JK

164 return 1 1JK

165 except PermissionError: 1I

166 print("Input Error. DataGraph file not readable.", file=sys.stderr) 1I

167 return 1 1I

168 return _run(args, data, options) 1xGHyzAfbvgwCDEhjkdlemnacFBopqrsitu

169 

170 

171def _validation_output(result, rdf_format): 1Ob

172 conforms, report, text = result 1xyzAfbvwjkdlemnacBopqrsitu

173 if rdf_format == "table" and isinstance(report, Graph): 1xyzAfbvwjkdlemnacBopqrsitu

174 return report_table(conforms, report) 1ac

175 if rdf_format != "human" and isinstance(report, Graph): 1xyzAfbvwjkdlemnBopqrsitu

176 return serialize_graph(report, rdf_format) 1fjkdlemnopqrsitu

177 return text 1xyzAbvwB

178 

179 

180def _run(args, inputs, options): 1Ob

181 data = inputs[0] if len(inputs) == 1 else inputs 1xGHyzAfbvgwCDEhjkdlemnacFBopqrsitu

182 try: 1xGHyzAfbvgwCDEhjkdlemnacFBopqrsitu

183 if args.rules: 1xGHyzAfbvgwCDEhjkdlemnacFBopqrsitu

184 options["advanced"] = True 1gCDEhF

185 report = shacl_rules(data, **options) 1gCDEhF

186 output_format = args.format if args.format not in {"human", "table"} else "trig" 1gCDEhF

187 text = serialize_graph(report, output_format) 1gCDEhF

188 conforms = True 1gCDEhF

189 else: 

190 options.update( 1xGHyzAfbvwjkdlemnacBopqrsitu

191 { 

192 name: getattr(args, name) 

193 for name in ["meta_shacl", "abort_on_first", "allow_infos", "allow_warnings"] 

194 } 

195 ) 

196 if args.validate_each: 1xGHyzAfbvwjkdlemnacBopqrsitu

197 results = validate_each(inputs, **options) 1B

198 conforms = all(result[0] for result in results.values()) 1B

199 text = "".join( 1B

200 f"=== Validation result for {index} ===\n" 

201 + _validation_output(result, args.format) 

202 + "\n" 

203 for index, result in results.items() 

204 ) 

205 else: 

206 result = validate(data, **options) 1xGHyzAfbvwjkdlemnacopqrsitu

207 conforms = result[0] 1xyzAfbvwjkdlemnacopqrsitu

208 text = _validation_output(result, args.format) 1xyzAfbvwjkdlemnacopqrsitu

209 if args.output: 1xyzAfbvgwCDEhjkdlemnacFBopqrsitu

210 with open(args.output, "w", encoding="utf-8") as output: 1f

211 output.write(text) 1f

212 else: 

213 sys.stdout.write(text) 1xyzAbvgwCDEhjkdlemnacFBopqrsitu

214 return 0 if conforms else 1 1xyzAfbvgwCDEhjkdlemnacFBopqrsitu

215 except NotImplementedError as error: 1GH

216 print(error, file=sys.stderr) 1H

217 return 3 1H

218 except (ValueError, RuntimeError, OSError) as error: 1G

219 print(error, file=sys.stderr) 1G

220 return 2 1G

221 

222 

223def entrypoint(): 1Ob

224 raise SystemExit(main()) 1bvw

225 

226 

227def rules_entrypoint(): 1Ob

228 raise SystemExit(main(["--rules", *sys.argv[1:]])) 1g

229 

230 

231if __name__ == "__main__": 1Ob

232 entrypoint() 1b