Coverage for presidio_cli / config.py: 95%
85 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 os
2from typing import Optional
4import pathspec
5import yaml
6from presidio_analyzer import AnalyzerEngine
9class PresidioCLIConfigError(Exception):
10 """Represents an error in the configuration file."""
12 pass
15class PresidioCLIConfig(object):
16 """Represents Presidio CLI configuration file. Reads the file and transforms the contents to class fields.""" # noqa: E501
18 def __init__(
19 self, content: Optional[str] = None, file: Optional[str] = None
20 ) -> None:
21 assert (content is None) ^ (file is None)
22 self.ignore = None
23 self.locale = None
24 self.analyzer = AnalyzerEngine()
25 self.threshold = 0
26 self.language = "en"
27 self.allow_list = []
28 if file is not None:
29 with open(file) as f:
30 content = f.read()
31 self.parse(content)
32 self.validate()
34 def is_file_ignored(self, filepath: str) -> bool:
35 """
36 Check if file should be processed by the Analyzer or ignored.
38 :param filepath: Path of file to be processed.
39 """
40 return self.ignore and filepath and self.ignore.match_file(filepath)
42 def is_text_file(self, filepath: str) -> bool:
43 """Detect if file is a not a binary file.Based on https://stackoverflow.com/a/7392391.
45 :param filepath: Path of the configuration file.
46 """
48 # Try to read the file as UTF-8.
49 # In case some invalid UTF-8 characters are found,
50 # an exception is caught and the file is not going
51 # to be processed.
52 try:
53 with open(filepath, newline="", encoding="utf-8") as f:
54 _ = f.read()
55 except UnicodeDecodeError:
56 return False
58 textchars = bytearray(
59 {7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7F}
60 )
61 with open(filepath, "rb") as f:
62 # return true if it's not a binary
63 return not bool(f.read(1024).translate(None, textchars))
65 def extend(self, base_config: "PresidioCLIConfig") -> None:
66 """
67 In case the configuration file is based on another file, append detected entities from base config to current config and overwrite language and ignored files with contents of base config.
69 :param base_config: PresidioCLIConfig object
70 """ # noqa: E501
71 assert isinstance(base_config, PresidioCLIConfig)
73 # Create list with unique entries
74 if base_config.entities is not None:
75 self.entities = list(set(base_config.entities + self.entities))
77 if base_config.ignore is not None:
78 self.ignore = base_config.ignore
80 if base_config.language is not None:
81 self.language = base_config.language
83 def parse(self, raw_content: str) -> None:
84 """
85 Read the content of YAML file and save the properties in class fields.
87 :param raw_content: String with the contents of configuration file.
88 """
89 try:
90 conf = yaml.safe_load(raw_content)
91 except Exception as e:
92 raise PresidioCLIConfigError("invalid config: %s" % e)
94 if not isinstance(conf, dict):
95 raise PresidioCLIConfigError("invalid config: not a dict")
97 self.entities = conf.get("entities", {})
99 if self.entities == {}:
100 self.entities = self.analyzer.get_supported_entities()
102 if "threshold" in conf:
103 if not 0 <= float(self.threshold) <= 1:
104 raise PresidioCLIConfigError(
105 f"Invalid threshold value: {self.threshold}. "
106 f"Threshold must be between 0 and 1"
107 )
108 self.threshold = float(conf["threshold"])
109 if "allow" in conf:
110 self.allow_list = conf["allow"]
111 if "language" in conf:
112 self.language = conf["language"]
113 if "extends" in conf:
114 path = get_extended_config_file(conf["extends"])
115 base = PresidioCLIConfig(file=path)
116 try:
117 self.extend(base)
118 except Exception as e:
119 raise PresidioCLIConfigError("invalid config: %s" % e)
121 if "ignore" in conf:
122 if not isinstance(conf["ignore"], str):
123 raise PresidioCLIConfigError(
124 "invalid config: ignore should contain file patterns"
125 )
126 self.ignore = pathspec.PathSpec.from_lines(
127 "gitwildmatch", conf["ignore"].splitlines()
128 )
130 if "locale" in conf:
131 if not isinstance(conf["locale"], str):
132 raise PresidioCLIConfigError(
133 "invalid config: locale should be a string"
134 )
135 self.locale = conf["locale"]
137 def validate(self) -> None:
138 """Check if entities requested to be detected in input file are supported by Presidio.""" # noqa: E501
139 for id in self.entities:
140 try:
141 assert id in self.analyzer.get_supported_entities()
142 except Exception:
143 raise PresidioCLIConfigError("invalid config: no such entity %s" % id)
146def get_extended_config_file(name: str) -> str:
147 """
148 Check if the configuration file is one of sample configs or if it is a file supplied by the user.
150 :param name: file name
151 :return: Full path of configuration file
152 """ # noqa: E501
153 # Is it a standard conf shipped with yamllint...
154 if "/" not in name:
155 std_conf = os.path.join(
156 os.path.dirname(os.path.realpath(__file__)), "conf", name + ".yaml"
157 )
159 if os.path.isfile(std_conf):
160 return std_conf
162 # or a custom conf on filesystem?
163 return name