Files
wehub-resource-sync 76d991c447
Auto Update PR / update-prs (push) Has been cancelled
CI / format-check (push) Has been cancelled
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / live-api-tests (push) Has been cancelled
CI / plugin-integration-test (push) Has been cancelled
CI / ollama-integration-test (push) Has been cancelled
CI / test-fork-pr (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:14 +08:00

954 lines
29 KiB
Python

# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for the main package functions in __init__.py."""
import textwrap
from unittest import mock
import warnings
from absl.testing import absltest
from absl.testing import parameterized
from langextract import prompting
import langextract as lx
from langextract.core import base_model
from langextract.core import data
from langextract.core import format_handler as fh
from langextract.core import schema
from langextract.core import types
from langextract.providers import schemas
class InitTest(parameterized.TestCase):
"""Test cases for the main package functions."""
@mock.patch.object(
schemas.gemini.GeminiSchema, "from_examples", autospec=True
)
@mock.patch("langextract.extraction.factory.create_model")
def test_lang_extract_as_lx_extract(
self, mock_create_model, mock_gemini_schema
):
input_text = "Patient takes Aspirin 100mg every morning."
mock_model = mock.MagicMock()
mock_model.infer.return_value = [[
types.ScoredOutput(
output=textwrap.dedent("""\
```json
{
"extractions": [
{
"entity": "Aspirin",
"entity_attributes": {
"class": "medication"
}
},
{
"entity": "100mg",
"entity_attributes": {
"frequency": "every morning",
"class": "dosage"
}
}
]
}
```"""),
score=0.9,
)
]]
mock_model.requires_fence_output = True
mock_create_model.return_value = mock_model
mock_gemini_schema.return_value = None
expected_result = data.AnnotatedDocument(
document_id=None,
extractions=[
data.Extraction(
extraction_class="entity",
extraction_text="Aspirin",
char_interval=data.CharInterval(start_pos=14, end_pos=21),
alignment_status=data.AlignmentStatus.MATCH_EXACT,
extraction_index=1,
group_index=0,
description=None,
attributes={"class": "medication"},
),
data.Extraction(
extraction_class="entity",
extraction_text="100mg",
char_interval=data.CharInterval(start_pos=22, end_pos=27),
alignment_status=data.AlignmentStatus.MATCH_EXACT,
extraction_index=2,
group_index=1,
description=None,
attributes={"frequency": "every morning", "class": "dosage"},
),
],
text="Patient takes Aspirin 100mg every morning.",
)
mock_description = textwrap.dedent("""\
Extract medication and dosage information in order of occurrence.
""")
mock_examples = [
lx.data.ExampleData(
text="Patient takes Tylenol 500mg daily.",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="Tylenol",
attributes={
"type": "analgesic",
"class": "medication",
},
),
],
)
]
mock_prompt_template = prompting.PromptTemplateStructured(
description=mock_description, examples=mock_examples
)
format_handler = fh.FormatHandler(
format_type=data.FormatType.JSON,
use_wrapper=True,
wrapper_key="extractions",
use_fences=True,
)
prompt_generator = prompting.QAPromptGenerator(
template=mock_prompt_template, format_handler=format_handler
)
actual_result = lx.extract(
text_or_documents=input_text,
prompt_description=mock_description,
examples=mock_examples,
api_key="some_api_key",
fence_output=True,
use_schema_constraints=False,
)
mock_gemini_schema.assert_not_called()
mock_create_model.assert_called_once()
mock_model.infer.assert_called_once_with(
batch_prompts=[prompt_generator.render(input_text)],
max_workers=10,
)
self.assertDataclassEqual(expected_result, actual_result)
@mock.patch("langextract.extraction.resolver.Resolver.align")
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_resolver_params_alignment_passthrough(
self, mock_create_model, mock_align
):
mock_model = mock.MagicMock()
mock_model.infer.return_value = [
[types.ScoredOutput(output='{"extractions":[]}')]
]
mock_model.requires_fence_output = False
mock_create_model.return_value = mock_model
mock_align.return_value = []
mock_examples = [
lx.data.ExampleData(
text="Patient takes Tylenol 500mg daily.",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="Tylenol",
attributes={
"type": "analgesic",
"class": "medication",
},
),
],
)
]
lx.extract(
text_or_documents="test text",
prompt_description="desc",
examples=mock_examples,
api_key="test_key",
resolver_params={
"enable_fuzzy_alignment": False,
"fuzzy_alignment_threshold": 0.8,
"accept_match_lesser": False,
},
)
mock_align.assert_called()
_, kwargs = mock_align.call_args
self.assertFalse(kwargs.get("enable_fuzzy_alignment"))
self.assertEqual(kwargs.get("fuzzy_alignment_threshold"), 0.8)
self.assertFalse(kwargs.get("accept_match_lesser"))
@mock.patch("langextract.annotation.Annotator.annotate_text")
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_resolver_params_suppress_parse_errors(
self, mock_create_model, mock_annotate
):
"""Test that suppress_parse_errors can be passed through resolver_params."""
mock_model = mock.MagicMock()
mock_model.requires_fence_output = False
mock_model.schema = None
mock_create_model.return_value = mock_model
mock_annotate.return_value = lx.data.AnnotatedDocument(
text="test", extractions=[]
)
mock_examples = [
lx.data.ExampleData(
text="Example text",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="example",
),
],
)
]
# This should not raise a TypeError about unknown key
lx.extract(
text_or_documents="test text",
prompt_description="desc",
examples=mock_examples,
api_key="test_key",
resolver_params={
"suppress_parse_errors": True,
"enable_fuzzy_alignment": False,
},
)
mock_annotate.assert_called()
_, kwargs = mock_annotate.call_args
self.assertIn("suppress_parse_errors", kwargs)
self.assertTrue(kwargs.get("suppress_parse_errors"))
self.assertFalse(kwargs.get("enable_fuzzy_alignment"))
@parameterized.named_parameters(
dict(
testcase_name="default_true",
resolver_params=None,
expected=True,
),
dict(
testcase_name="caller_override_false",
resolver_params={"suppress_parse_errors": False},
expected=False,
),
)
@mock.patch("langextract.annotation.Annotator.annotate_text", autospec=True)
@mock.patch("langextract.extraction.factory.create_model", autospec=True)
def test_extract_suppress_parse_errors_routing(
self, mock_create_model, mock_annotate, resolver_params, expected
):
mock_model = mock.MagicMock()
mock_model.requires_fence_output = False
mock_model.schema = None
mock_create_model.return_value = mock_model
mock_annotate.return_value = lx.data.AnnotatedDocument(
text="test", extractions=[]
)
mock_examples = [
lx.data.ExampleData(
text="Example text",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="example",
),
],
)
]
extract_kwargs = {
"text_or_documents": "test text",
"prompt_description": "desc",
"examples": mock_examples,
"api_key": "test_key",
}
if resolver_params is not None:
extract_kwargs["resolver_params"] = resolver_params
lx.extract(**extract_kwargs)
mock_annotate.assert_called()
_, kwargs = mock_annotate.call_args
self.assertEqual(kwargs.get("suppress_parse_errors"), expected)
@mock.patch("langextract.extraction.resolver.Resolver")
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_resolver_params_none_handling(
self, mock_create_model, mock_resolver_class
):
mock_model = mock.MagicMock()
mock_model.infer.return_value = [
[types.ScoredOutput(output='{"extractions":[]}')]
]
mock_model.requires_fence_output = False
mock_create_model.return_value = mock_model
mock_resolver = mock.MagicMock()
mock_resolver_class.return_value = mock_resolver
mock_examples = [
lx.data.ExampleData(
text="Test text",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="test",
),
],
)
]
with mock.patch(
"langextract.annotation.Annotator.annotate_text"
) as mock_annotate:
mock_annotate.return_value = lx.data.AnnotatedDocument(
text="test", extractions=[]
)
lx.extract(
text_or_documents="test text",
prompt_description="desc",
examples=mock_examples,
api_key="test_key",
resolver_params={
"enable_fuzzy_alignment": None,
"fuzzy_alignment_threshold": 0.8,
},
)
_, resolver_kwargs = mock_resolver_class.call_args
self.assertNotIn("enable_fuzzy_alignment", resolver_kwargs)
self.assertNotIn("fuzzy_alignment_threshold", resolver_kwargs)
self.assertIn("format_handler", resolver_kwargs)
_, annotate_kwargs = mock_annotate.call_args
self.assertNotIn("enable_fuzzy_alignment", annotate_kwargs)
self.assertEqual(annotate_kwargs["fuzzy_alignment_threshold"], 0.8)
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_resolver_params_typo_error(self, mock_create_model):
mock_model = mock.MagicMock()
mock_model.requires_fence_output = False
mock_create_model.return_value = mock_model
mock_examples = [
lx.data.ExampleData(
text="Test",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="test",
),
],
)
]
with self.assertRaisesRegex(TypeError, "Unknown key in resolver_params"):
lx.extract(
text_or_documents="test",
prompt_description="desc",
examples=mock_examples,
api_key="test_key",
resolver_params={
"fuzzy_alignment_treshold": ( # Typo: treshold instead of threshold
0.5
),
},
)
@mock.patch("langextract.annotation.Annotator.annotate_documents")
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_resolver_params_docs_path_passthrough(
self, mock_create_model, mock_annotate_docs
):
mock_model = mock.MagicMock()
mock_model.infer.return_value = [
[types.ScoredOutput(output='{"extractions":[]}')]
]
mock_model.requires_fence_output = False
mock_create_model.return_value = mock_model
mock_annotate_docs.return_value = []
docs = [lx.data.Document(text="doc1")]
examples = [
lx.data.ExampleData(
text="Example text",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="example",
),
],
)
]
lx.extract(
text_or_documents=docs,
prompt_description="desc",
examples=examples,
api_key="k",
resolver_params={
"enable_fuzzy_alignment": False,
"fuzzy_alignment_threshold": 0.9,
"accept_match_lesser": False,
},
)
_, kwargs = mock_annotate_docs.call_args
self.assertFalse(kwargs.get("enable_fuzzy_alignment"))
self.assertEqual(kwargs.get("fuzzy_alignment_threshold"), 0.9)
self.assertFalse(kwargs.get("accept_match_lesser"))
@mock.patch("langextract.annotation.Annotator.annotate_text")
@mock.patch("langextract.extraction.resolver.Resolver")
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_resolver_params_none_threshold(
self, mock_create_model, mock_resolver_cls, mock_annotate
):
mock_model = mock.MagicMock()
mock_model.infer.return_value = [
[types.ScoredOutput(output='{"extractions":[]}')]
]
mock_model.requires_fence_output = False
mock_create_model.return_value = mock_model
mock_resolver_cls.return_value = mock.MagicMock()
mock_annotate.return_value = lx.data.AnnotatedDocument(
text="t", extractions=[]
)
lx.extract(
text_or_documents="t",
prompt_description="d",
examples=[
lx.data.ExampleData(
text="example",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="ex",
),
],
)
],
api_key="k",
resolver_params={"fuzzy_alignment_threshold": None},
)
_, resolver_kwargs = mock_resolver_cls.call_args
self.assertNotIn("fuzzy_alignment_threshold", resolver_kwargs)
_, annotate_kwargs = mock_annotate.call_args
self.assertNotIn("fuzzy_alignment_threshold", annotate_kwargs)
@mock.patch.object(
schemas.gemini.GeminiSchema, "from_examples", autospec=True
)
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_custom_params_reach_inference(
self, mock_create_model, mock_gemini_schema
):
"""Sanity check that custom parameters reach the inference layer."""
input_text = "Test text"
mock_model = mock.MagicMock()
mock_model.infer.return_value = [[
types.ScoredOutput(
output='```json\n{"extractions": []}\n```',
score=0.9,
)
]]
mock_model.requires_fence_output = True
mock_create_model.return_value = mock_model
mock_gemini_schema.return_value = None
mock_examples = [
lx.data.ExampleData(
text="Example",
extractions=[
lx.data.Extraction(
extraction_class="test",
extraction_text="example",
),
],
)
]
lx.extract(
text_or_documents=input_text,
prompt_description="Test extraction",
examples=mock_examples,
api_key="test_key",
max_workers=5,
fence_output=True,
use_schema_constraints=False,
)
mock_model.infer.assert_called_once()
_, kwargs = mock_model.infer.call_args
self.assertEqual(kwargs.get("max_workers"), 5)
@mock.patch("langextract.extraction.factory.create_model")
def test_extract_with_custom_tokenizer(self, mock_create_model):
"""Test that a custom tokenizer can be passed to extract()."""
input_text = "Test text"
mock_model = mock.MagicMock()
mock_model.infer.return_value = [[
types.ScoredOutput(
output='```json\n{"extractions": []}\n```',
score=0.9,
)
]]
mock_model.requires_fence_output = True
mock_create_model.return_value = mock_model
def mock_tokenize(text):
if text == "\u241F": # Delimiter
return lx.tokenizer.TokenizedText(
text=text,
tokens=[
lx.tokenizer.Token(
index=0,
token_type=lx.tokenizer.TokenType.PUNCTUATION,
char_interval=lx.tokenizer.CharInterval(0, 1),
)
],
)
# Return dummy tokens for other text to avoid "empty tokens" error in aligner
return lx.tokenizer.TokenizedText(
text=text,
tokens=[
lx.tokenizer.Token(
index=0,
token_type=lx.tokenizer.TokenType.WORD,
char_interval=lx.tokenizer.CharInterval(0, len(text)),
)
],
)
mock_tokenizer = mock.MagicMock()
mock_tokenizer.tokenize.side_effect = mock_tokenize
mock_examples = [
lx.data.ExampleData(
text="Example",
extractions=[
lx.data.Extraction(
extraction_class="test",
extraction_text="example",
),
],
)
]
lx.extract(
text_or_documents=input_text,
prompt_description="Test extraction",
examples=mock_examples,
api_key="test_key",
tokenizer=mock_tokenizer,
)
mock_tokenizer.tokenize.assert_called_with(input_text)
def test_data_module_exports_via_compatibility_shim(self):
"""Verify data module exports are accessible via lx.data."""
expected_exports = [
"AlignmentStatus",
"CharInterval",
"Extraction",
"Document",
"AnnotatedDocument",
"ExampleData",
"FormatType",
]
for name in expected_exports:
with self.subTest(export=name):
self.assertTrue(
hasattr(lx.data, name),
f"lx.data.{name} not accessible via compatibility shim",
)
def test_tokenizer_module_exports_via_compatibility_shim(self):
"""Verify tokenizer module exports are accessible via lx.tokenizer."""
expected_exports = [
"BaseTokenizerError",
"InvalidTokenIntervalError",
"SentenceRangeError",
"CharInterval",
"TokenInterval",
"TokenType",
"Token",
"TokenizedText",
"tokenize",
"tokens_text",
"find_sentence_range",
]
for name in expected_exports:
with self.subTest(export=name):
self.assertTrue(
hasattr(lx.tokenizer, name),
f"lx.tokenizer.{name} not accessible via compatibility shim",
)
@parameterized.named_parameters(
dict(
testcase_name="show_progress_true_debug_false",
show_progress=True,
debug=False,
expected_progress_disabled=False,
),
dict(
testcase_name="show_progress_false_debug_false",
show_progress=False,
debug=False,
expected_progress_disabled=True,
),
dict(
testcase_name="show_progress_true_debug_true",
show_progress=True,
debug=True,
expected_progress_disabled=False,
),
dict(
testcase_name="show_progress_false_debug_true",
show_progress=False,
debug=True,
expected_progress_disabled=True,
),
)
@mock.patch("langextract.progress.create_extraction_progress_bar")
@mock.patch("langextract.extraction.factory.create_model")
def test_show_progress_controls_progress_bar(
self,
mock_create_model,
mock_progress,
show_progress,
debug,
expected_progress_disabled,
):
"""Test that show_progress parameter controls progress bar visibility."""
mock_model = mock.MagicMock()
mock_model.infer.return_value = [
[
types.ScoredOutput(
output='{"extractions": []}',
score=0.9,
)
]
]
mock_model.requires_fence_output = False
mock_create_model.return_value = mock_model
mock_progress.side_effect = lambda iterable, **kwargs: iter(iterable)
mock_examples = [
lx.data.ExampleData(
text="Example text",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="example",
),
],
)
]
lx.extract(
text_or_documents="test text",
prompt_description="extract entities",
examples=mock_examples,
api_key="test_key",
show_progress=show_progress,
debug=debug,
)
mock_progress.assert_called()
call_args = mock_progress.call_args
self.assertEqual(
call_args.kwargs.get("disable", False), expected_progress_disabled
)
@mock.patch("langextract.factory.create_model")
def test_schema_validation_warning_issued(self, mock_create_model):
"""Test that schema validation warnings are properly issued."""
mock_model = mock.Mock(spec=base_model.BaseLanguageModel)
mock_model.requires_fence_output = True
mock_model.infer.return_value = [
[types.ScoredOutput(output='{"extractions": []}', score=1.0)]
]
mock_schema = mock.Mock(spec=schema.BaseSchema)
def validate_format_side_effect(format_handler):
warnings.warn("Test validation warning", UserWarning, stacklevel=3)
mock_schema.validate_format = mock.Mock(
side_effect=validate_format_side_effect
)
mock_model.schema = mock_schema
mock_create_model.return_value = mock_model
test_examples = [
lx.data.ExampleData(
text="test",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="test",
),
],
)
]
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = lx.extract(
text_or_documents="Sample text",
prompt_description="Extract",
examples=test_examples,
model_id="test-model",
api_key="key",
use_schema_constraints=True,
)
warning_messages = [str(warning.message) for warning in w]
self.assertIn(
"Test validation warning",
" ".join(warning_messages),
"Schema validation warning should be issued",
)
self.assertIsNotNone(result)
def test_gemini_schema_deprecation_warning(self):
"""Test that passing gemini_schema triggers deprecation warning."""
mock_model = mock.MagicMock(spec=base_model.BaseLanguageModel)
mock_model.infer.return_value = iter(
[[mock.Mock(output='{"extractions": []}')]]
)
mock_model.requires_fence_output = True
mock_model.schema = None
self.enter_context(
mock.patch(
"langextract.factory.create_model",
return_value=mock_model,
)
)
self.enter_context(
mock.patch(
"langextract.annotation.Annotator.annotate_text",
return_value=data.AnnotatedDocument(text="test", extractions=[]),
)
)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
_ = lx.extract(
text_or_documents="test",
prompt_description="Extract conditions",
examples=[
lx.data.ExampleData(
text="test",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="test",
),
],
)
],
model_id="gemini-3.5-flash",
api_key="test_key",
language_model_params={"gemini_schema": "deprecated"},
)
self.assertTrue(
any(
issubclass(warning.category, FutureWarning)
and "gemini_schema" in str(warning.message)
for warning in w
),
"Expected deprecation warning for gemini_schema",
)
class AnnotationSuppressParseErrorsTest(absltest.TestCase):
"""Tests for suppress_parse_errors isolation in the annotation layer."""
def setUp(self):
super().setUp()
self._mock_model = mock.MagicMock()
self._mock_model.requires_fence_output = False
self._mock_model.schema = None
examples = [
lx.data.ExampleData(
text="Example text",
extractions=[
lx.data.Extraction(
extraction_class="entity",
extraction_text="example",
),
],
)
]
handler = fh.FormatHandler(
format_type=data.FormatType.JSON, use_wrapper=True
)
self._annotator = lx.annotation.Annotator(
language_model=self._mock_model,
prompt_template=prompting.PromptTemplateStructured(
description="desc", examples=examples
),
format_handler=handler,
)
self._resolver = lx.resolver.Resolver(format_handler=handler)
def test_suppress_parse_errors_not_sent_to_infer(self):
"""Provider infer() must not receive suppress_parse_errors."""
self._mock_model.infer.return_value = [
[types.ScoredOutput(output='{"extractions": []}')]
]
_ = self._annotator.annotate_text(
text="test text",
resolver=self._resolver,
suppress_parse_errors=True,
)
self._mock_model.infer.assert_called()
_, infer_kwargs = self._mock_model.infer.call_args
self.assertNotIn("suppress_parse_errors", infer_kwargs)
def test_warning_excludes_raw_chunk_text(self):
"""Suppressed parse error logs must not contain raw document text."""
sensitive_text = "Patient has diabetes and hypertension"
self._mock_model.infer.return_value = [
[types.ScoredOutput(output="I cannot extract entities")]
]
with mock.patch("langextract.resolver.logging") as mock_logging:
_ = self._annotator.annotate_text(
text=sensitive_text,
resolver=self._resolver,
suppress_parse_errors=True,
)
for call in mock_logging.warning.call_args_list:
log_message = str(call)
self.assertNotIn(sensitive_text, log_message)
def test_mixed_chunks_valid_extractions_survive(self):
"""Valid extractions survive when other chunks fail to parse."""
self._mock_model.infer.return_value = [
[
types.ScoredOutput(
output='{"extractions": [{"entity": "diabetes"}]}'
)
],
[types.ScoredOutput(output="I don't see any entities.")],
]
result = self._annotator.annotate_text(
text="The patient has diabetes and takes insulin daily",
resolver=self._resolver,
max_char_buffer=25,
suppress_parse_errors=True,
)
self.assertIsNotNone(result)
self.assertGreater(len(result.extractions), 0)
self.assertEqual(result.extractions[0].extraction_class, "entity")
def test_mixed_chunks_raises_when_suppression_disabled(self):
"""Unparseable chunk raises when suppress_parse_errors=False."""
self._mock_model.infer.return_value = [
[types.ScoredOutput(output="No entities here.")],
]
with self.assertRaises(lx.resolver.ResolverParsingError):
self._annotator.annotate_text(
text="Test text",
resolver=self._resolver,
suppress_parse_errors=False,
)
class FetchUrlsOptInTest(absltest.TestCase):
"""URL fetching must be opt-in to keep the library SSRF-safe by default."""
def setUp(self):
super().setUp()
self._example = lx.data.ExampleData(
text="hi",
extractions=[
lx.data.Extraction(extraction_class="thing", extraction_text="hi")
],
)
def _extract(self, **overrides):
kwargs = dict(
text_or_documents="http://example.com/doc",
prompt_description="x",
examples=[self._example],
model_id="gemini-3.5-flash",
api_key="fake",
)
kwargs.update(overrides)
return lx.extract(**kwargs)
@mock.patch("langextract.extraction.factory.create_model", autospec=True)
@mock.patch("langextract.extraction.io.download_text_from_url", autospec=True)
def test_url_is_not_fetched_by_default(self, downloader, create_model):
sentinel = RuntimeError("short-circuit after URL decision")
create_model.side_effect = sentinel
with self.assertRaises(RuntimeError) as cm:
self._extract()
self.assertIs(cm.exception, sentinel)
downloader.assert_not_called()
@mock.patch("langextract.extraction.io.download_text_from_url", autospec=True)
def test_fetch_urls_true_invokes_downloader(self, downloader):
sentinel = RuntimeError("download invoked")
downloader.side_effect = sentinel
with self.assertRaises(RuntimeError) as cm:
self._extract(fetch_urls=True)
self.assertIs(cm.exception, sentinel)
downloader.assert_called_once_with("http://example.com/doc")
if __name__ == "__main__":
absltest.main()