diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/ner/huggingface_ner_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/ner/huggingface_ner_recognizer.py index f91fcb6473..f862122926 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/ner/huggingface_ner_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/ner/huggingface_ner_recognizer.py @@ -257,17 +257,13 @@ def load(self) -> None: This method handles: 1. Hardware acceleration setup (CUDA validation and fallback) 2. Lazy-loading of the heavyweight ML pipeline. - - :raises ValueError: If model_name is not set """ if self.ner_pipeline is not None: return if not self.model_name: - raise ValueError( - "model_name must be set before calling load(). " - "Pass it to __init__() or set it directly." - ) + logger.info("model_name is not set. Deferring HuggingFace model loading.") + return # Device validation and fallback device = self.device @@ -430,6 +426,13 @@ def analyze( # Defensive guard for entities input entities = entities or [] + if not self.model_name: + raise ValueError( + "model_name must be specified to analyze text using " + "HuggingFaceNerRecognizer. Specify model_name when instantiating " + "HuggingFaceNerRecognizer or in default_recognizers.yaml." + ) + if not self.ner_pipeline: self.load() diff --git a/presidio-analyzer/tests/test_huggingface_ner_recognizer.py b/presidio-analyzer/tests/test_huggingface_ner_recognizer.py index 40368d93cc..da6cf073b0 100644 --- a/presidio-analyzer/tests/test_huggingface_ner_recognizer.py +++ b/presidio-analyzer/tests/test_huggingface_ner_recognizer.py @@ -256,10 +256,14 @@ def test_hf_recognizer_load_errors(): with pytest.raises(ImportError): HuggingFaceNerRecognizer(model_name="test") - # 2. Test ValueError when model_name is missing - with patch(path, new=MagicMock()): - with pytest.raises(ValueError, match="model_name must be set"): - HuggingFaceNerRecognizer(model_name=None) + +@pytest.mark.usefixtures("mock_torch_installed") +def test_analyze_without_model_name_raises_with_configuration_pointer(): + """Verify calling analyze() on HuggingFaceNerRecognizer without model_name raises ValueError with configuration pointer.""" + with patch(HF_PIPELINE_PATH, new=MagicMock()): + recognizer = HuggingFaceNerRecognizer(model_name=None) + with pytest.raises(ValueError, match="default_recognizers.yaml"): + recognizer.analyze("Some text to analyze", ["PERSON"]) @pytest.mark.usefixtures("mock_torch_installed") @@ -723,3 +727,13 @@ def test_hf_recognizer_resolves_deferred_tokenizer_chunker(mock_pipeline): assert chunker.tokenizer is mock_tokenizer assert chunker.max_tokens == 128 assert chunker.overlap_tokens == 16 + + +@pytest.mark.usefixtures("mock_torch_installed") +def test_registry_builds_with_huggingface_entry_enabled(): + """Verify HuggingFaceNerRecognizer can be instantiated without model_name during registry build.""" + with patch(HF_PIPELINE_PATH, new=MagicMock()): + recognizer = HuggingFaceNerRecognizer() + assert recognizer.model_name is None + assert recognizer.ner_pipeline is None + diff --git a/presidio-analyzer/tests/test_recognizers_loader_utils.py b/presidio-analyzer/tests/test_recognizers_loader_utils.py index 987b5c04d3..b8d1f445b5 100644 --- a/presidio-analyzer/tests/test_recognizers_loader_utils.py +++ b/presidio-analyzer/tests/test_recognizers_loader_utils.py @@ -607,15 +607,11 @@ def test_yaml_country_code_blank_value_raises(): LOADER_KWARGS = ("name", "supported_language") # Entries that cannot load from their shipped configuration even with every -# dependency installed, so the load test below cannot cover them. -# -# ``HuggingFaceNerRecognizer``: ``EntityRecognizer.__init__`` calls ``load()`` -# unconditionally and ``load()`` requires ``model_name``, which the shipped -# entry does not supply -- it raises ValueError once ``transformers`` and -# ``torch`` are present. That is a pre-existing defect in the entry, not -# something this contract can assert away, and adding ``model_name`` here would -# make the test download a model. It stays covered by the resolve test. -NOT_LOADABLE_FROM_SHIPPED_ENTRY = {"HuggingFaceNerRecognizer"} +# dependency installed, so the load test below cannot cover them. Empty today: +# ``HuggingFaceNerRecognizer`` used to sit here because ``load()`` raised when the +# shipped entry supplied no ``model_name``. It now defers loading instead, so the +# load test covers it like every other entry. +NOT_LOADABLE_FROM_SHIPPED_ENTRY: set[str] = set() # Entries gated behind an optional dependency, for which refusing to load with an # actionable ImportError is the intended behavior. The skip is scoped to these