diff --git a/e2e-tests/tests/test_api_analyzer.py b/e2e-tests/tests/test_api_analyzer.py index 730179ac40..fdb0506acf 100644 --- a/e2e-tests/tests/test_api_analyzer.py +++ b/e2e-tests/tests/test_api_analyzer.py @@ -112,6 +112,27 @@ def test_given_a_incorrect_analyze_language_input_then_return_error(): assert equal_json_strings(expected_response, response_content) +@pytest.mark.api +def test_given_partially_unsupported_entities_then_return_supported_results(): + request_body = """ + { + "text": "codice fiscale RSSMRA85M01H501Q, IBAN IT60X0542811101000000123456", + "language": "en", + "entities": ["IT_FISCAL_CODE", "IBAN_CODE"] + } + """ + + response_status, response_content = analyze(request_body) + + expected_response = """ + [ + {"entity_type": "IBAN_CODE", "start": 38, "end": 65, "score": 1.0, "analysis_explanation": null} + ] + """ + assert response_status == 200 + assert equal_json_strings(expected_response, response_content) + + @pytest.mark.api def test_given_a_correlationid_analyze_input_then_return_normal_response(): request_body = """ diff --git a/presidio-analyzer/presidio_analyzer/analyzer_engine.py b/presidio-analyzer/presidio_analyzer/analyzer_engine.py index 8637c6524f..77c5c79579 100644 --- a/presidio-analyzer/presidio_analyzer/analyzer_engine.py +++ b/presidio-analyzer/presidio_analyzer/analyzer_engine.py @@ -207,6 +207,13 @@ def analyze( :param regex_flags: regex flags to be used for when allow_list_match is "regex" :param nlp_artifacts: precomputed NlpArtifacts :return: an array of the found entities in the text + :raises ValueError: if no recognizers match the requested entities and language. + + Unsupported entities are ignored with a logged warning, while supported + entities are analyzed. Ignoring unsupported entities is deprecated and will + raise an error in a future version. Call + `AnalyzerEngine.get_supported_entities(language)` to find supported entities, + or provide matching ad-hoc recognizers. :Example: diff --git a/presidio-analyzer/presidio_analyzer/recognizer_registry/recognizer_registry.py b/presidio-analyzer/presidio_analyzer/recognizer_registry/recognizer_registry.py index 130b5a44f3..4c8eac506a 100644 --- a/presidio-analyzer/presidio_analyzer/recognizer_registry/recognizer_registry.py +++ b/presidio-analyzer/presidio_analyzer/recognizer_registry/recognizer_registry.py @@ -201,6 +201,13 @@ def get_recognizers( as part of the request :return: A list of the recognizers which supports the supplied entities and language + :raises ValueError: if language is None, entities is None when all_fields + is False, or no recognizers match the request. + + Unsupported entities are ignored with a logged warning. This behavior + is deprecated and will raise an error in a future version. Use + AnalyzerEngine.get_supported_entities(language) to find supported entities, + or provide matching ad-hoc recognizers. """ if language is None: raise ValueError("No language provided") @@ -232,7 +239,10 @@ def get_recognizers( if not subset: logger.warning( "Entity %s doesn't have the corresponding" - " recognizer in language : %s", + " recognizer in language : %s. Ignoring unsupported entities" + " is deprecated and will raise an error in a future version." + " Use AnalyzerEngine.get_supported_entities(language) to find" + " supported entities, or add a matching recognizer.", entity, language, ) diff --git a/presidio-analyzer/tests/test_analyzer_engine.py b/presidio-analyzer/tests/test_analyzer_engine.py index e567c1e5ab..bae95e1bf8 100644 --- a/presidio-analyzer/tests/test_analyzer_engine.py +++ b/presidio-analyzer/tests/test_analyzer_engine.py @@ -18,8 +18,10 @@ ) from presidio_analyzer.nlp_engine import ( NlpArtifacts, + NoOpNlpEngine, SpacyNlpEngine, ) +from presidio_analyzer.predefined_recognizers import CreditCardRecognizer from presidio_analyzer.recognizer_registry import RecognizerRegistryProvider # noqa: F401 @@ -1274,3 +1276,38 @@ def test_when_regex_allow_list_is_all_empty_entries_then_results_are_kept(): ) assert filtered == results + + +@pytest.mark.parametrize( + "entities", + [["CREDIT_CARD"], ["CREDIT_CARD", "UNSUPPORTED_ENTITY"]], +) +def test_when_analyze_with_supported_entities_then_return_exact_results( + entities, + caplog, +): + analyzer_engine = AnalyzerEngine( + registry=RecognizerRegistry([CreditCardRecognizer()]), + nlp_engine=NoOpNlpEngine(models=[{"lang_code": "en", "model_name": "no_op"}]), + ) + with caplog.at_level("WARNING", logger="presidio-analyzer"): + results = analyzer_engine.analyze( + text="My name is David and his number is 4095-2609-9393-4932", + entities=entities, + language="en", + ) + + assert len(results) == 1 + assert_result(results[0], "CREDIT_CARD", 35, 54, 1.0) + warnings = [ + record.message for record in caplog.records if record.levelname == "WARNING" + ] + if "UNSUPPORTED_ENTITY" in entities: + assert len(warnings) == 1 + assert "UNSUPPORTED_ENTITY" in warnings[0] + assert "language : en" in warnings[0] + assert "deprecated" in warnings[0] + assert "will raise an error in a future version" in warnings[0] + assert "get_supported_entities" in warnings[0] + else: + assert warnings == [] diff --git a/presidio-analyzer/tests/test_app.py b/presidio-analyzer/tests/test_app.py new file mode 100644 index 0000000000..ebc9277455 --- /dev/null +++ b/presidio-analyzer/tests/test_app.py @@ -0,0 +1,79 @@ +# ruff: noqa: D103,E501 +"""Tests for the analyzer REST API server.""" + +from unittest.mock import patch + +import pytest +from app import create_app +from presidio_analyzer import AnalyzerEngine, RecognizerRegistry +from presidio_analyzer.nlp_engine import NoOpNlpEngine +from presidio_analyzer.predefined_recognizers import IbanRecognizer + + +@pytest.fixture +def client(): + engine = AnalyzerEngine( + registry=RecognizerRegistry([IbanRecognizer()]), + nlp_engine=NoOpNlpEngine(models=[{"lang_code": "en", "model_name": "no_op"}]), + ) + with patch("app.AnalyzerEngineProvider") as provider: + provider.return_value.create_engine.return_value = engine + app = create_app() + app.testing = True + yield app.test_client() + + +@pytest.mark.parametrize("entities", [["IBAN_CODE"], ["IT_FISCAL_CODE", "IBAN_CODE"]]) +def test_given_supported_entities_then_return_exact_results(client, entities, caplog): + with caplog.at_level("WARNING", logger="presidio-analyzer"): + response = client.post( + "/analyze", + json={ + "text": "codice fiscale RSSMRA85M01H501Q, IBAN IT60X0542811101000000123456", + "language": "en", + "entities": entities, + }, + ) + + assert response.status_code == 200 + assert response.get_json() == [ + { + "entity_type": "IBAN_CODE", + "start": 38, + "end": 65, + "score": 1.0, + "analysis_explanation": None, + } + ] + warnings = [ + record.message for record in caplog.records if record.levelname == "WARNING" + ] + if "IT_FISCAL_CODE" in entities: + assert len(warnings) == 1 + assert "IT_FISCAL_CODE" in warnings[0] + assert "language : en" in warnings[0] + assert "deprecated" in warnings[0] + assert "will raise an error in a future version" in warnings[0] + else: + assert warnings == [] + + +def test_given_no_matching_entities_then_return_500(client): + response = client.post( + "/analyze", + json={"text": "test", "language": "en", "entities": ["UNSUPPORTED_ENTITY"]}, + ) + + assert response.status_code == 500 + assert response.get_json() == { + "error": "No matching recognizers were found to serve the request." + } + + +def test_given_unsupported_language_for_supported_entities_then_return_500(client): + response = client.get("/supportedentities?language=he") + + assert response.status_code == 500 + assert response.get_json() == { + "error": "No matching recognizers were found to serve the request." + } diff --git a/presidio-analyzer/tests/test_recognizer_registry.py b/presidio-analyzer/tests/test_recognizer_registry.py index c61d7aa857..dccec77b48 100644 --- a/presidio-analyzer/tests/test_recognizer_registry.py +++ b/presidio-analyzer/tests/test_recognizer_registry.py @@ -75,10 +75,14 @@ def test_when_get_recognizers_then_return_all_fields(mock_recognizer_registry): def test_when_get_recognizers_one_language_then_return_one_entity( mock_recognizer_registry, + caplog, ): registry = mock_recognizer_registry recognizers = registry.get_recognizers(language="de", entities=["PERSON"]) - assert len(recognizers) == 1 + assert recognizers == [registry.recognizers[1]] + assert not any( + "Ignoring unsupported entities" in record.message for record in caplog.records + ) def test_when_get_recognizers_unsupported_language_then_return( @@ -89,6 +93,63 @@ def test_when_get_recognizers_unsupported_language_then_return( registry.get_recognizers(language="brrrr", entities=["PERSON"]) +@pytest.mark.parametrize("unsupported_entity", ["UNSUPPORTED_ENTITY", "ADDRESS"]) +def test_when_get_recognizers_with_unsupported_entity_then_warn_and_return_supported( + mock_recognizer_registry, + caplog, + unsupported_entity, +): + registry = mock_recognizer_registry + with caplog.at_level("WARNING", logger="presidio-analyzer"): + recognizers = registry.get_recognizers( + language="en", entities=["PERSON", unsupported_entity] + ) + + assert recognizers == [registry.recognizers[0]] + warnings = [ + record.message for record in caplog.records if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + assert unsupported_entity in warnings[0] + assert "language : en" in warnings[0] + assert "deprecated" in warnings[0] + assert "future version" in warnings[0] + assert "raise" in warnings[0] + assert "get_supported_entities" in warnings[0] + + +@pytest.mark.parametrize("entity", ["ADDRESS", "UNSUPPORTED_ENTITY"]) +def test_when_get_recognizers_without_matching_entities_then_raise( + mock_recognizer_registry, + entity, +): + registry = mock_recognizer_registry + with pytest.raises(ValueError) as err: + registry.get_recognizers(language="en", entities=[entity]) + + assert str(err.value) == "No matching recognizers were found to serve the request." + + +def test_when_get_recognizers_with_ad_hoc_recognizer_then_no_error( + mock_recognizer_registry, + caplog, +): + registry = mock_recognizer_registry + ad_hoc_recognizer = create_mock_pattern_recognizer( + "en", "UNSUPPORTED_ENTITY", "ad hoc" + ) + recognizers = registry.get_recognizers( + language="en", + entities=["UNSUPPORTED_ENTITY"], + ad_hoc_recognizers=[ad_hoc_recognizer], + ) + + assert recognizers == [ad_hoc_recognizer] + assert not any( + "Ignoring unsupported entities" in record.message for record in caplog.records + ) + + def test_when_get_recognizers_specific_language_and_entity_then_return_one_result( mock_recognizer_registry, ):