Coverage for presidio_analyzer / recognizer_registry / recognizer_registry.py: 87%

114 statements  

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

1import copy 

2import logging 

3from pathlib import Path 

4from typing import Dict, Iterable, List, Optional, Type, Union 

5 

6import regex as re 

7import yaml 

8 

9from presidio_analyzer import EntityRecognizer, PatternRecognizer 

10from presidio_analyzer.nlp_engine import ( 

11 NlpEngine, 

12 SpacyNlpEngine, 

13 StanzaNlpEngine, 

14 TransformersNlpEngine, 

15) 

16from presidio_analyzer.predefined_recognizers import ( 

17 SpacyRecognizer, 

18 StanzaRecognizer, 

19 TransformersRecognizer, 

20) 

21from presidio_analyzer.recognizer_registry.recognizers_loader_utils import ( 

22 RecognizerConfigurationLoader, 

23 RecognizerListLoader, 

24) 

25 

26logger = logging.getLogger("presidio-analyzer") 

27 

28 

29class RecognizerRegistry: 

30 """ 

31 Detect, register and hold all recognizers to be used by the analyzer. 

32 

33 :param recognizers: An optional list of recognizers, 

34 that will be available instead of the predefined recognizers 

35 :param global_regex_flags: regex flags to be used in regex matching, 

36 including deny-lists 

37 :param supported_languages: List of languages supported by this registry. 

38 

39 """ 

40 

41 def __init__( 

42 self, 

43 recognizers: Optional[Iterable[EntityRecognizer]] = None, 

44 global_regex_flags: Optional[int] = re.DOTALL | re.MULTILINE | re.IGNORECASE, 

45 supported_languages: Optional[List[str]] = None, 

46 ): 

47 if recognizers: 

48 self.recognizers = recognizers 

49 else: 

50 self.recognizers = [] 

51 self.global_regex_flags = global_regex_flags 

52 self.supported_languages = ( 

53 supported_languages if supported_languages else ["en"] 

54 ) 

55 

56 def _create_nlp_recognizer( 

57 self, 

58 nlp_engine: Optional[NlpEngine] = None, 

59 supported_language: Optional[str] = None, 

60 ) -> SpacyRecognizer: 

61 nlp_recognizer = self.get_nlp_recognizer(nlp_engine) 

62 

63 if nlp_engine: 

64 return nlp_recognizer( 

65 supported_language=supported_language, 

66 supported_entities=nlp_engine.get_supported_entities(), 

67 ) 

68 

69 return nlp_recognizer(supported_language=supported_language) 

70 

71 def add_nlp_recognizer(self, nlp_engine: NlpEngine) -> None: 

72 """ 

73 Adding NLP recognizer in accordance with the nlp engine. 

74 

75 :param nlp_engine: The NLP engine. 

76 :return: None 

77 """ 

78 

79 if not nlp_engine: 

80 supported_languages = self.supported_languages 

81 else: 

82 supported_languages = nlp_engine.get_supported_languages() 

83 

84 self.recognizers.extend( 

85 [ 

86 self._create_nlp_recognizer( 

87 nlp_engine=nlp_engine, supported_language=supported_language 

88 ) 

89 for supported_language in supported_languages 

90 ] 

91 ) 

92 

93 def load_predefined_recognizers( 

94 self, languages: Optional[List[str]] = None, nlp_engine: NlpEngine = None 

95 ) -> None: 

96 """ 

97 Load the existing recognizers into memory. 

98 

99 :param languages: List of languages for which to load recognizers 

100 :param nlp_engine: The NLP engine to use. 

101 :return: None 

102 """ 

103 

104 registry_configuration = {"global_regex_flags": self.global_regex_flags} 

105 if languages is not None: 

106 registry_configuration["supported_languages"] = languages 

107 

108 configuration = RecognizerConfigurationLoader.get( 

109 registry_configuration=registry_configuration 

110 ) 

111 recognizers = RecognizerListLoader.get(**configuration) 

112 

113 self.recognizers.extend(recognizers) 

114 self.add_nlp_recognizer(nlp_engine=nlp_engine) 

115 

116 @staticmethod 

117 def get_nlp_recognizer( 

118 nlp_engine: NlpEngine, 

119 ) -> Type[SpacyRecognizer]: 

120 """Return the recognizer leveraging the selected NLP Engine.""" 

121 

122 if isinstance(nlp_engine, StanzaNlpEngine): 

123 return StanzaRecognizer 

124 if isinstance(nlp_engine, TransformersNlpEngine): 

125 return TransformersRecognizer 

126 if not nlp_engine or isinstance(nlp_engine, SpacyNlpEngine): 

127 return SpacyRecognizer 

128 else: 

129 logger.warning( 

130 "nlp engine should be either SpacyNlpEngine," 

131 "StanzaNlpEngine or TransformersNlpEngine" 

132 ) 

133 # Returning default 

134 return SpacyRecognizer 

135 

136 def get_recognizers( 

137 self, 

138 language: str, 

139 entities: Optional[List[str]] = None, 

140 all_fields: bool = False, 

141 ad_hoc_recognizers: Optional[List[EntityRecognizer]] = None, 

142 ) -> List[EntityRecognizer]: 

143 """ 

144 Return a list of recognizers which supports the specified name and language. 

145 

146 :param entities: the requested entities 

147 :param language: the requested language 

148 :param all_fields: a flag to return all fields of a requested language. 

149 :param ad_hoc_recognizers: Additional recognizers provided by the user 

150 as part of the request 

151 :return: A list of the recognizers which supports the supplied entities 

152 and language 

153 """ 

154 if language is None: 

155 raise ValueError("No language provided") 

156 

157 if entities is None and all_fields is False: 

158 raise ValueError("No entities provided") 

159 

160 all_possible_recognizers = copy.copy(self.recognizers) 

161 if ad_hoc_recognizers: 

162 all_possible_recognizers.extend(ad_hoc_recognizers) 

163 

164 # filter out unwanted recognizers 

165 to_return = set() 

166 if all_fields: 

167 to_return = [ 

168 rec 

169 for rec in all_possible_recognizers 

170 if language == rec.supported_language 

171 ] 

172 else: 

173 for entity in entities: 

174 subset = [ 

175 rec 

176 for rec in all_possible_recognizers 

177 if entity in rec.supported_entities 

178 and language == rec.supported_language 

179 ] 

180 

181 if not subset: 

182 logger.warning( 

183 "Entity %s doesn't have the corresponding" 

184 " recognizer in language : %s", 

185 entity, 

186 language, 

187 ) 

188 else: 

189 to_return.update(set(subset)) 

190 

191 logger.debug( 

192 "Returning a total of %s recognizers", 

193 str(len(to_return)), 

194 ) 

195 

196 if not to_return: 

197 raise ValueError("No matching recognizers were found to serve the request.") 

198 

199 return list(to_return) 

200 

201 def add_recognizer(self, recognizer: EntityRecognizer) -> None: 

202 """ 

203 Add a new recognizer to the list of recognizers. 

204 

205 :param recognizer: Recognizer to add 

206 """ 

207 if not isinstance(recognizer, EntityRecognizer): 

208 raise ValueError("Input is not of type EntityRecognizer") 

209 

210 self.recognizers.append(recognizer) 

211 

212 def remove_recognizer( 

213 self, recognizer_name: str, language: Optional[str] = None 

214 ) -> None: 

215 """ 

216 Remove a recognizer based on its name. 

217 

218 :param recognizer_name: Name of recognizer to remove 

219 :param language: The supported language of the recognizer to be removed, 

220 in case multiple recognizers with the same name are present, 

221 and only one should be removed. 

222 """ 

223 

224 if not language: 

225 new_recognizers = [ 

226 rec for rec in self.recognizers if rec.name != recognizer_name 

227 ] 

228 

229 logger.info( 

230 "Removed %s recognizers which had the name %s", 

231 str(len(self.recognizers) - len(new_recognizers)), 

232 recognizer_name, 

233 ) 

234 

235 else: 

236 new_recognizers = [ 

237 rec 

238 for rec in self.recognizers 

239 if rec.name != recognizer_name or rec.supported_language != language 

240 ] 

241 

242 logger.info( 

243 "Removed %s recognizers which had the name %s and language %s", 

244 str(len(self.recognizers) - len(new_recognizers)), 

245 recognizer_name, 

246 language, 

247 ) 

248 

249 self.recognizers = new_recognizers 

250 

251 def add_pattern_recognizer_from_dict(self, recognizer_dict: Dict) -> None: 

252 """ 

253 Load a pattern recognizer from a Dict into the recognizer registry. 

254 

255 :param recognizer_dict: Dict holding a serialization of an PatternRecognizer 

256 

257 :example: 

258 >>> registry = RecognizerRegistry() 

259 >>> recognizer = { "name": "Titles Recognizer", "supported_language": "en","supported_entity": "TITLE", "deny_list": ["Mr.","Mrs."]} 

260 >>> registry.add_pattern_recognizer_from_dict(recognizer) 

261 """ # noqa: E501 

262 

263 recognizer = PatternRecognizer.from_dict(recognizer_dict) 

264 self.add_recognizer(recognizer) 

265 

266 def add_recognizers_from_yaml(self, yml_path: Union[str, Path]) -> None: 

267 r""" 

268 Read YAML file and load recognizers into the recognizer registry. 

269 

270 See example yaml file here: 

271 https://github.com/microsoft/presidio/blob/main/presidio-analyzer/presidio_analyzer/conf/example_recognizers.yaml 

272 

273 :example: 

274 >>> yaml_file = "recognizers.yaml" 

275 >>> registry = RecognizerRegistry() 

276 >>> registry.add_recognizers_from_yaml(yaml_file) 

277 

278 """ 

279 

280 try: 

281 with open(yml_path) as stream: 

282 yaml_recognizers = yaml.safe_load(stream) 

283 

284 for yaml_recognizer in yaml_recognizers["recognizers"]: 

285 self.add_pattern_recognizer_from_dict(yaml_recognizer) 

286 except OSError as io_error: 

287 print(f"Error reading file {yml_path}") 

288 raise io_error 

289 except yaml.YAMLError as yaml_error: 

290 print(f"Failed to parse file {yml_path}") 

291 raise yaml_error 

292 except TypeError as yaml_error: 

293 print(f"Failed to parse file {yml_path}") 

294 raise yaml_error 

295 

296 def __instantiate_recognizer( 

297 self, recognizer_class: Type[EntityRecognizer], supported_language: str 

298 ): 

299 """ 

300 Instantiate a recognizer class given type and input. 

301 

302 :param recognizer_class: Class object of the recognizer 

303 :param supported_language: Language this recognizer should support 

304 """ 

305 

306 inst = recognizer_class(supported_language=supported_language) 

307 if isinstance(inst, PatternRecognizer): 

308 inst.global_regex_flags = self.global_regex_flags 

309 return inst 

310 

311 def _get_supported_languages(self) -> List[str]: 

312 languages = [] 

313 for rec in self.recognizers: 

314 languages.append(rec.supported_language) 

315 

316 return list(set(languages)) 

317 

318 def get_supported_entities( 

319 self, languages: Optional[List[str]] = None 

320 ) -> List[str]: 

321 """ 

322 Return the supported entities by the set of recognizers loaded. 

323 

324 :param languages: The languages to get the supported entities for. 

325 If languages=None, returns all entities for all languages. 

326 """ 

327 if not languages: 

328 languages = self._get_supported_languages() 

329 

330 supported_entities = [] 

331 for language in languages: 

332 recognizers = self.get_recognizers(language=language, all_fields=True) 

333 

334 for recognizer in recognizers: 

335 supported_entities.extend(recognizer.get_supported_entities()) 

336 

337 return list(set(supported_entities))