11import re
2+ import json
23
34import pytest
45
@@ -27,7 +28,7 @@ def test_given_url_at_the_end_then_we_redact_is_successfully():
2728 ]
2829 expected_result = (
2930 '{"text": "The url is ", "items": [{"start": 11, "end": 11, "entity_type": '
30- '"URL", "text": "", "operator": "redact"}]}'
31+ '"URL", "text": "", "operator": "redact", "score": 1.0 }]}'
3132 )
3233 run_engine_and_validate (text , anonymizer_config , analyzer_results , expected_result )
3334
@@ -63,9 +64,9 @@ def test_given_name_and_phone_number_then_we_anonymize_correctly():
6364 expected_result = (
6465 '{"text": "hello world, my name is ********. My number is: '
6566 '03-******4", "items": [{"start": 48, "end": 57, "entity_type": '
66- '"PHONE_NUMBER", "text": "03-******", "operator": "mask"}, '
67+ '"PHONE_NUMBER", "text": "03-******", "operator": "mask", "score": 0.95 }, '
6768 '{"start": 24, "end": 32, "entity_type": "NAME", '
68- '"text": "********", "operator": "mask"}]}'
69+ '"text": "********", "operator": "mask", "score": 0.8 }]}'
6970 )
7071 run_engine_and_validate (text , anonymizer_config , analyzer_results , expected_result )
7172
@@ -85,9 +86,9 @@ def test_given_name_and_phone_number_without_anonymizers_then_we_use_default():
8586 '{"text": "hello world, my name is <NAME>. My number is: '
8687 '<PHONE_NUMBER>4", "items": [{"start": 46, "end": 60, '
8788 '"entity_type": "PHONE_NUMBER", "text": "<PHONE_NUMBER>", '
88- '"operator": "replace"}, {"start": 24, "end": 30, '
89+ '"operator": "replace", "score": 0.95 }, {"start": 24, "end": 30, '
8990 '"entity_type": "NAME", "text": "<NAME>", '
90- '"operator": "replace"}]}'
91+ '"operator": "replace", "score": 0.8 }]}'
9192 )
9293 run_engine_and_validate (text , anonymizer_config , analyzer_results , expected_result )
9394
@@ -106,9 +107,9 @@ def test_given_redact_and_replace_then_we_anonymize_successfully():
106107 '{"text": "hello world, my name is . My number is: '
107108 '<PHONE_NUMBER>4", "items": [{"start": 40, "end": 54, '
108109 '"entity_type": "PHONE_NUMBER", "text": "<PHONE_NUMBER>", '
109- '"operator": "replace"}, {"start": 24, "end": 24, '
110+ '"operator": "replace", "score": 0.95 }, {"start": 24, "end": 24, '
110111 '"entity_type": "NAME", "text": "", "operator": '
111- '"redact"}]}'
112+ '"redact", "score": 0.8 }]}'
112113 )
113114 run_engine_and_validate (text , anonymizer_config , analyzer_results , expected_result )
114115
@@ -128,13 +129,13 @@ def test_given_intersecting_entities_then_we_anonymize_correctly():
128129 '{"text": "hello world, my name is <FULL_NAME><LAST_NAME> My '
129130 'number is: <PHONE_NUMBER><SSN>4", "items": [{"start": 75, '
130131 '"end": 80, "entity_type": "SSN", "text": "<SSN>", '
131- '"operator": "replace"}, {"start": 61, "end": 75, '
132+ '"operator": "replace", "score": 0.8 }, {"start": 61, "end": 75, '
132133 '"entity_type": "PHONE_NUMBER", "text": "<PHONE_NUMBER>", '
133- '"operator": "replace"}, {"start": 35, "end": 46, '
134+ '"operator": "replace", "score": 0.95 }, {"start": 35, "end": 46, '
134135 '"entity_type": "LAST_NAME", "text": "<LAST_NAME>", '
135- '"operator": "replace"}, {"start": 24, "end": 35, '
136+ '"operator": "replace", "score": 0.6 }, {"start": 24, "end": 35, '
136137 '"entity_type": "FULL_NAME", "text": "<FULL_NAME>", '
137- '"operator": "replace"}]}'
138+ '"operator": "replace", "score": 0.6 }]}'
138139 )
139140 run_engine_and_validate (text , anonymizer_config , analyzer_results , expected_result )
140141
@@ -149,7 +150,7 @@ def test_given_intersecting_the_same_entities_then_we_anonymize_correctly():
149150 expected_result = (
150151 '{"text": "hello world, my name is <FULL_NAME> My number is: 03-4453334", '
151152 '"items": [{"start": 24, "end": 35, "entity_type": "FULL_NAME",'
152- ' "text": "<FULL_NAME>", "operator": "replace"}]}'
153+ ' "text": "<FULL_NAME>", "operator": "replace", "score": 0.6 }]}'
153154 )
154155 run_engine_and_validate (text , anonymizer_config , analyzer_results , expected_result )
155156
@@ -318,7 +319,6 @@ def test_when_hash_with_user_provided_salt_then_hash_is_reproducible():
318319)
319320def test_hash_with_known_salt_produces_expected_output (text , salt , hash_type , expected_hash ):
320321 """Test that hashing with a known salt produces expected deterministic output."""
321- from presidio_anonymizer import AnonymizerEngine
322322
323323 params = {"hash_type" : hash_type , "salt" : salt }
324324 anonymizer_config = {"DEFAULT" : OperatorConfig ("hash" , params )}
@@ -339,6 +339,95 @@ def test_hash_with_known_salt_produces_expected_output(text, salt, hash_type, ex
339339 assert result2 .items [0 ].text == expected_hash
340340
341341
342+ def test_given_single_entity_then_score_is_propagated_to_result ():
343+ """Score from analyzer result appears on the corresponding OperatorResult."""
344+ text = "My name is Jane Doe"
345+ anonymizer_config = {"PERSON" : OperatorConfig ("replace" )}
346+ analyzer_results = [
347+ RecognizerResult (start = 11 , end = 19 , score = 0.85 , entity_type = "PERSON" ),
348+ ]
349+
350+ engine = AnonymizerEngine ()
351+ result = engine .anonymize (text , analyzer_results , anonymizer_config )
352+
353+ assert result .items [0 ].score == 0.85
354+
355+
356+ def test_given_multiple_entities_then_each_score_is_propagated_correctly ():
357+ """Each OperatorResult carries the score of its originating RecognizerResult."""
358+ text = "My name is Jane Doe. My number is 034453334"
359+ anonymizer_config = {}
360+ analyzer_results = [
361+ RecognizerResult (start = 11 , end = 19 , score = 0.8 , entity_type = "NAME" ),
362+ RecognizerResult (start = 34 , end = 43 , score = 0.95 , entity_type = "PHONE_NUMBER" ),
363+ ]
364+
365+ engine = AnonymizerEngine ()
366+ result = engine .anonymize (text , analyzer_results , anonymizer_config )
367+
368+ result_by_type = {item .entity_type : item for item in result .items }
369+ assert result_by_type ["NAME" ].score == 0.8
370+ assert result_by_type ["PHONE_NUMBER" ].score == 0.95
371+
372+
373+ @pytest .mark .parametrize (
374+ "operator_name,operator_params" ,
375+ [
376+ ("replace" , {}),
377+ ("redact" , {}),
378+ ("mask" , {"masking_char" : "*" , "chars_to_mask" : 4 , "from_end" : False }),
379+ ("hash" , {}),
380+ ],
381+ )
382+ def test_given_different_operators_then_score_is_always_propagated (
383+ operator_name , operator_params
384+ ):
385+ """Score is propagated regardless of which operator is applied."""
386+ text = "My name is Jane Doe"
387+ anonymizer_config = {"NAME" : OperatorConfig (operator_name , operator_params )}
388+ analyzer_results = [
389+ RecognizerResult (start = 11 , end = 19 , score = 0.75 , entity_type = "NAME" ),
390+ ]
391+
392+ engine = AnonymizerEngine ()
393+ result = engine .anonymize (text , analyzer_results , anonymizer_config )
394+
395+ assert result .items [0 ].score == 0.75
396+
397+
398+ def test_given_conflicting_entities_then_winning_entity_score_is_preserved ():
399+ """When conflict resolution drops an entity, the surviving entity keeps its own score."""
400+ text = "hello world, my name is Jane Doe"
401+ anonymizer_config = {}
402+ analyzer_results = [
403+ RecognizerResult (start = 24 , end = 32 , score = 0.6 , entity_type = "FULL_NAME" ),
404+ RecognizerResult (start = 24 , end = 28 , score = 0.9 , entity_type = "FIRST_NAME" ), # loses
405+ RecognizerResult (start = 24 , end = 30 , score = 0.8 , entity_type = "NAME" ), # loses
406+ ]
407+
408+ engine = AnonymizerEngine ()
409+ result = engine .anonymize (text , analyzer_results , anonymizer_config )
410+
411+ assert len (result .items ) == 1
412+ assert result .items [0 ].entity_type == "FULL_NAME"
413+ assert result .items [0 ].score == 0.6 # its own score, not the score of dropped entities
414+
415+
416+ def test_given_score_in_result_then_it_is_present_in_json_output ():
417+ """Score is included in to_json() serialized output."""
418+ text = "My name is Jane Doe"
419+ anonymizer_config = {}
420+ analyzer_results = [
421+ RecognizerResult (start = 11 , end = 19 , score = 0.85 , entity_type = "NAME" ),
422+ ]
423+
424+ engine = AnonymizerEngine ()
425+ result = engine .anonymize (text , analyzer_results , anonymizer_config )
426+
427+ output = json .loads (result .to_json ())
428+ assert output ["items" ][0 ]["score" ] == pytest .approx (0.85 )
429+
430+
342431def run_engine_and_validate (
343432 text : str , anonymizers_config , analyzer_results , expected_result
344433):
0 commit comments