Coverage for presidio_cli / cli.py: 85%

137 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-29 08:58 +0000

1import argparse 

2import io 

3import json 

4import locale 

5import os 

6import platform 

7import sys 

8import traceback 

9from typing import Generator, List 

10 

11from presidio_cli import APP_DESCRIPTION, APP_VERSION, SHELL_NAME 

12from presidio_cli.analyzer import PIIProblem, analyze 

13from presidio_cli.config import PresidioCLIConfig, PresidioCLIConfigError 

14 

15 

16class Format(object): 

17 """Class providing methods for formatting output, with information about discovered PII problems.""" # noqa: E501 

18 

19 @staticmethod 

20 def parsable(problem: PIIProblem) -> str: 

21 """ 

22 Format the problem as JSON. 

23 

24 :param problem: PIIProblem to be formatted. 

25 :return: JSON representation of the problem. 

26 """ 

27 return json.dumps(problem.recognizer_result) 

28 

29 @staticmethod 

30 def standard(problem: PIIProblem) -> str: 

31 """ 

32 Output the problem in standard format. 

33 

34 :param problem: PIIProblem to be formatted. 

35 :return: Standard text representation of the problem. 

36 """ 

37 line = " %d:%d" % (problem.line, problem.column) 

38 line += max(12 - len(line), 0) * " " 

39 line += str(problem.score) 

40 line += max(21 - len(line), 0) * " " 

41 line += problem.type 

42 if problem.explanation: 

43 line += " (%s)" % problem.explanation 

44 return line 

45 

46 @staticmethod 

47 def standard_color(problem: PIIProblem) -> str: 

48 """ 

49 Output the problem in standard, colored format. 

50 

51 :param problem: PIIProblem to be formatted. 

52 :return: Standard, colored text representation of the problem. 

53 """ 

54 line = " \033[2m%d:%d\033[0m" % (problem.line, problem.column) 

55 line += max(20 - len(line), 0) * " " 

56 if problem.score < 1: # warning 

57 line += "\033[33m%s\033[0m" % problem.score 

58 else: 

59 line += "\033[31m%s\033[0m" % problem.score 

60 line += max(38 - len(line), 0) * " " 

61 line += problem.type 

62 if problem.explanation: 

63 line += " \033[2m(%s)\033[0m" % problem.explanation 

64 return line 

65 

66 @staticmethod 

67 def github(problem: PIIProblem, filename: str) -> str: 

68 """ 

69 Output the problem in git-diff-like format. 

70 

71 :param problem: PIIProblem to be formatted. 

72 :param filename: Filename where the problem occurs. 

73 """ 

74 line = ( 

75 f"::{str(problem.score)} file={filename},line={format(problem.line)}," 

76 + f"col={format(problem.column)}::{format(problem.line)}" 

77 + f":{format(problem.column)} [{problem.type}]" 

78 ) 

79 if problem.explanation: 

80 line += problem.explanation 

81 return line 

82 

83 

84def supports_color() -> bool: 

85 """Check whether the platform supports colored output.""" 

86 supported_platform = not ( 

87 platform.system() == "Windows" 

88 and not ( 

89 "ANSICON" in os.environ 

90 or ("TERM" in os.environ and os.environ["TERM"] == "ANSI") 

91 ) 

92 ) 

93 return supported_platform and hasattr(sys.stdout, "isatty") and sys.stdout.isatty() 

94 

95 

96def show_problems( 

97 problems: Generator["PIIProblem", None, None], 

98 file: str, 

99 args_format: str, 

100 no_warn: bool, 

101): 

102 """ 

103 Show formatted output of discovered problems. 

104 

105 :param problems: generator of PIIProblem objects 

106 :param file: processed filename for 'stdin' 

107 :param args_format: format in which to output discovered problems 

108 :param no_warn: whether to output only error level problems 

109 """ 

110 max_level = 0 

111 first = True 

112 

113 if args_format == "auto": 

114 if "GITHUB_ACTIONS" in os.environ and "GITHUB_WORKFLOW" in os.environ: 

115 args_format = "github" 

116 elif supports_color(): 

117 args_format = "colored" 

118 

119 for problem in problems: 

120 if no_warn and (problem.level != "error"): 

121 continue 

122 if args_format == "parsable": 

123 print(Format.parsable(problem)) 

124 elif args_format == "github": 

125 if first: 

126 print("::group::%s" % file) 

127 first = False 

128 print(Format.github(problem, file)) 

129 elif args_format == "colored": 

130 if first: 

131 print("\033[4m%s\033[0m" % file) 

132 first = False 

133 print(Format.standard_color(problem)) 

134 else: 

135 if first: 

136 print(file) 

137 first = False 

138 print(Format.standard(problem)) 

139 

140 if not first and args_format == "github": 

141 print("::endgroup::") 

142 

143 if not first and args_format != "parsable": 

144 print("") 

145 

146 return max_level 

147 

148 

149def find_files_recursively( 

150 items: List[str], conf: PresidioCLIConfig 

151) -> Generator[str, None, None]: 

152 """ 

153 Generate all file names inside the directories. 

154 

155 :param items: List of directories containing files to be analyzed. 

156 :param conf: PresidioCLIConfig object 

157 """ 

158 for item in items: 

159 if os.path.isdir(item): 

160 for root, dirnames, filenames in os.walk(item): 

161 for f in filenames: 

162 filepath = os.path.join(root, f) 

163 if conf.is_text_file(filepath): 

164 yield filepath 

165 else: 

166 if conf.is_text_file(item): 

167 yield item 

168 

169 

170def run() -> None: 

171 """Entrypoint of Presidio CLI.""" 

172 parser = argparse.ArgumentParser(prog=SHELL_NAME, description=APP_DESCRIPTION) 

173 

174 parser.add_argument("-v", "--version", action="version", version=f"v{APP_VERSION}") 

175 

176 files_group = parser.add_mutually_exclusive_group(required=True) 

177 files_group.add_argument( 

178 "files", 

179 metavar="FILE_OR_DIR", 

180 nargs="*", 

181 default=(), 

182 help="files to check", 

183 ) 

184 files_group.add_argument( 

185 "-", action="store_true", dest="stdin", help="read from standard input" 

186 ) 

187 

188 config_group = parser.add_mutually_exclusive_group() 

189 config_group.add_argument( 

190 "-c", 

191 "--config-file", 

192 dest="config_file", 

193 action="store", 

194 help="path to a custom configuration", 

195 ) 

196 

197 config_group.add_argument( 

198 "-d", 

199 "--config-data", 

200 dest="config_data", 

201 action="store", 

202 help="custom configuration (as YAML source)", 

203 ) 

204 

205 parser.add_argument( 

206 "-f", 

207 "--format", 

208 choices=("standard", "github", "auto", "colored", "parsable"), 

209 default="auto", 

210 help="format for parsing output", 

211 ) 

212 

213 parser.add_argument( 

214 "--no-warnings", 

215 action="store_true", 

216 help="output only error level problems", 

217 ) 

218 

219 args = parser.parse_args() 

220 

221 try: 

222 if args.config_data is not None: 

223 if args.config_data != "" and ":" not in args.config_data: 

224 args.config_data = "extends: " + args.config_data 

225 conf = PresidioCLIConfig(content=args.config_data) 

226 elif args.config_file is not None: 

227 conf = PresidioCLIConfig(file=args.config_file) 

228 elif os.path.isfile(".presidiocli"): 

229 conf = PresidioCLIConfig(file=".presidiocli") 

230 else: 

231 conf = PresidioCLIConfig(content="extends: default") 

232 except PresidioCLIConfigError as e: 

233 print(e, file=sys.stderr) 

234 sys.exit(1) 

235 

236 if conf.locale is not None: 

237 locale.setlocale(locale.LC_ALL, conf.locale) 

238 

239 prob_num = 0 

240 for file in find_files_recursively(args.files, conf): 

241 filepath = file[2:] if file.startswith("./") or file.startswith(".\\") else file 

242 try: 

243 with io.open(file, newline="", encoding="utf-8") as f: 

244 problems = analyze(f, conf, filepath) 

245 except Exception: 

246 traceback.print_exc() 

247 continue 

248 prob_num = show_problems( 

249 problems, file, args_format=args.format, no_warn=args.no_warnings 

250 ) 

251 

252 if args.stdin: 

253 try: 

254 problems = analyze(sys.stdin, conf, "") 

255 except EnvironmentError as e: 

256 print(e, file=sys.stderr) 

257 sys.exit(1) 

258 prob_num = show_problems( 

259 problems, 

260 "stdin", 

261 args_format=args.format, 

262 no_warn=args.no_warnings, 

263 ) 

264 

265 if prob_num > 0: 

266 return_code = 1 

267 else: 

268 return_code = 0 

269 sys.exit(return_code)