Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
22 changes: 18 additions & 4 deletions presidio-analyzer/tests/test_huggingface_ner_recognizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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

14 changes: 5 additions & 9 deletions presidio-analyzer/tests/test_recognizers_loader_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down