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
« 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
11from presidio_cli import APP_DESCRIPTION, APP_VERSION, SHELL_NAME
12from presidio_cli.analyzer import PIIProblem, analyze
13from presidio_cli.config import PresidioCLIConfig, PresidioCLIConfigError
16class Format(object):
17 """Class providing methods for formatting output, with information about discovered PII problems.""" # noqa: E501
19 @staticmethod
20 def parsable(problem: PIIProblem) -> str:
21 """
22 Format the problem as JSON.
24 :param problem: PIIProblem to be formatted.
25 :return: JSON representation of the problem.
26 """
27 return json.dumps(problem.recognizer_result)
29 @staticmethod
30 def standard(problem: PIIProblem) -> str:
31 """
32 Output the problem in standard format.
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
46 @staticmethod
47 def standard_color(problem: PIIProblem) -> str:
48 """
49 Output the problem in standard, colored format.
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
66 @staticmethod
67 def github(problem: PIIProblem, filename: str) -> str:
68 """
69 Output the problem in git-diff-like format.
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
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()
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.
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
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"
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))
140 if not first and args_format == "github":
141 print("::endgroup::")
143 if not first and args_format != "parsable":
144 print("")
146 return max_level
149def find_files_recursively(
150 items: List[str], conf: PresidioCLIConfig
151) -> Generator[str, None, None]:
152 """
153 Generate all file names inside the directories.
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
170def run() -> None:
171 """Entrypoint of Presidio CLI."""
172 parser = argparse.ArgumentParser(prog=SHELL_NAME, description=APP_DESCRIPTION)
174 parser.add_argument("-v", "--version", action="version", version=f"v{APP_VERSION}")
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 )
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 )
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 )
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 )
213 parser.add_argument(
214 "--no-warnings",
215 action="store_true",
216 help="output only error level problems",
217 )
219 args = parser.parse_args()
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)
236 if conf.locale is not None:
237 locale.setlocale(locale.LC_ALL, conf.locale)
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 )
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 )
265 if prob_num > 0:
266 return_code = 1
267 else:
268 return_code = 0
269 sys.exit(return_code)