This is an automated email from the ASF dual-hosted git repository.
chamikaramj pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new fadfc789376 Removes usages of the sentence-transformers package...
(#40439)
fadfc789376 is described below
commit fadfc789376137afabab0469bf1e43a0f10a6ad0
Author: Chamikara Jayalath <[email protected]>
AuthorDate: Thu Oct 8 19:03:09 2026 -0700
Removes usages of the sentence-transformers package... (#40439)
---
.../apache_beam/ml/rag/embeddings/huggingface.py | 23 +---
.../ml/rag/embeddings/huggingface_test.py | 56 ++++++++
.../ml/transforms/embeddings/huggingface.py | 48 +++++--
.../ml/transforms/embeddings/huggingface_test.py | 142 +++++++++++++++++++++
sdks/python/apache_beam/testing/test_utils.py | 61 +++++++++
sdks/python/apache_beam/testing/test_utils_test.py | 31 +++++
sdks/python/apache_beam/yaml/yaml_ml_test.py | 40 +++++-
7 files changed, 368 insertions(+), 33 deletions(-)
diff --git a/sdks/python/apache_beam/ml/rag/embeddings/huggingface.py
b/sdks/python/apache_beam/ml/rag/embeddings/huggingface.py
index e17a5d66fe9..8ad34ca690b 100644
--- a/sdks/python/apache_beam/ml/rag/embeddings/huggingface.py
+++ b/sdks/python/apache_beam/ml/rag/embeddings/huggingface.py
@@ -32,11 +32,6 @@ from apache_beam.ml.transforms.base import
_ImageEmbeddingHandler
from apache_beam.ml.transforms.base import _TextEmbeddingHandler
from apache_beam.ml.transforms.embeddings.huggingface import
_SentenceTransformerModelHandler
-try:
- from sentence_transformers import SentenceTransformer
-except ImportError:
- SentenceTransformer = None
-
try:
from PIL import Image as PILImage
except ImportError:
@@ -66,20 +61,15 @@ class HuggingfaceTextEmbeddings(EmbeddingsManager):
``model.encode()``
(e.g. ``normalize_embeddings``).
"""
- if not SentenceTransformer:
- raise ImportError(
- "sentence-transformers is required to use "
- "HuggingfaceTextEmbeddings."
- "Please install it with using `pip install sentence-transformers`.")
super().__init__(type_adapter=create_text_adapter(), **kwargs)
self.model_name = model_name
self.max_seq_length = max_seq_length
- self.model_class = SentenceTransformer
def get_model_handler(self):
"""Returns model handler configured with RAG adapter."""
+ # model_class is not set, so that sentence-transformers is only imported
+ # when the model is loaded during pipeline execution.
return _SentenceTransformerModelHandler(
- model_class=self.model_class,
max_seq_length=self.max_seq_length,
model_name=self.model_name,
load_model_args=self.load_model_args,
@@ -165,11 +155,6 @@ class HuggingfaceImageEmbeddings(EmbeddingsManager):
``model.encode()``
(e.g. ``normalize_embeddings``).
"""
- if not SentenceTransformer:
- raise ImportError(
- "sentence-transformers is required to use "
- "HuggingfaceImageEmbeddings. "
- "Please install it with `pip install sentence-transformers`.")
if not PILImage:
raise ImportError(
"Pillow is required to use HuggingfaceImageEmbeddings. "
@@ -177,12 +162,12 @@ class HuggingfaceImageEmbeddings(EmbeddingsManager):
super().__init__(type_adapter=_create_hf_image_adapter(), **kwargs)
self.model_name = model_name
self.max_seq_length = max_seq_length
- self.model_class = SentenceTransformer
def get_model_handler(self):
"""Returns model handler configured with RAG adapter."""
+ # model_class is not set, so that sentence-transformers is only imported
+ # when the model is loaded during pipeline execution.
return _SentenceTransformerModelHandler(
- model_class=self.model_class,
max_seq_length=self.max_seq_length,
model_name=self.model_name,
load_model_args=self.load_model_args,
diff --git a/sdks/python/apache_beam/ml/rag/embeddings/huggingface_test.py
b/sdks/python/apache_beam/ml/rag/embeddings/huggingface_test.py
index 65c21771524..de93df95627 100644
--- a/sdks/python/apache_beam/ml/rag/embeddings/huggingface_test.py
+++ b/sdks/python/apache_beam/ml/rag/embeddings/huggingface_test.py
@@ -16,6 +16,7 @@
"""Tests for apache_beam.ml.rag.embeddings.huggingface."""
+import importlib.util
import io
import os
import shutil
@@ -33,6 +34,7 @@ from apache_beam.ml.rag.types import Content
from apache_beam.ml.rag.types import EmbeddableItem
from apache_beam.ml.rag.types import Embedding
from apache_beam.ml.transforms.base import MLTransform
+from apache_beam.testing import test_utils
from apache_beam.testing.test_pipeline import TestPipeline
from apache_beam.testing.util import assert_that
from apache_beam.testing.util import equal_to
@@ -234,6 +236,60 @@ class HuggingfaceImageEmbeddingsTest(unittest.TestCase):
shutil.rmtree(artifact_location)
+class HuggingfaceEmbeddingsJobSubmissionTest(unittest.TestCase):
+ """Tests that sentence-transformers is only used when executing pipelines.
+
+ sentence-transformers should only be needed for loading the model when the
+ pipeline is executed (for example, on the workers), but not for constructing
+ and submitting the pipeline. These tests don't require sentence-transformers
+ to be installed.
+ """
+ def setUp(self):
+ self.artifact_location = tempfile.mkdtemp(prefix='hf_job_submission_')
+
+ def tearDown(self):
+ shutil.rmtree(self.artifact_location)
+
+ def _construct_and_serialize_pipeline(self, items, embedder):
+ pipeline = beam.Pipeline()
+ _ = (
+ pipeline
+ | beam.Create(items)
+ | MLTransform(write_artifact_location=self.artifact_location).
+ with_transform(embedder))
+ # Serializes the pipeline, as done during job submission, and deserializes
+ # it again. Deserializing fails if the pipeline references (i.e. requires)
+ # a module that can't be imported.
+ beam.Pipeline.from_runner_api(
+ pipeline.to_runner_api(), pipeline.runner, pipeline.options)
+
+ def test_importing_module_does_not_import_sentence_transformers(self):
+ # Executes the module in a new module object, i.e. without replacing the
+ # already imported module.
+ spec = importlib.util.find_spec(HuggingfaceTextEmbeddings.__module__)
+ with test_utils.block_imports('sentence_transformers') as
attempted_imports:
+ spec.loader.exec_module(importlib.util.module_from_spec(spec))
+ self.assertEqual(attempted_imports, [])
+
+ def test_text_embeddings_job_submission_does_not_use_sentence_transformers(
+ self):
+ items = [Chunk(content=Content(text="This is a test sentence."), id="1")]
+ with test_utils.block_imports('sentence_transformers') as
attempted_imports:
+ embedder = HuggingfaceTextEmbeddings(
+ model_name="sentence-transformers/all-MiniLM-L6-v2")
+ self._construct_and_serialize_pipeline(items, embedder)
+ self.assertEqual(attempted_imports, [])
+
+ @unittest.skipIf(not PIL_AVAILABLE, "PIL not available")
+ def test_image_embeddings_job_submission_does_not_use_sentence_transformers(
+ self):
+ items = [EmbeddableItem.from_image(_create_png_bytes(), id="img1")]
+ with test_utils.block_imports('sentence_transformers') as
attempted_imports:
+ embedder = HuggingfaceImageEmbeddings(model_name="clip-ViT-B-32")
+ self._construct_and_serialize_pipeline(items, embedder)
+ self.assertEqual(attempted_imports, [])
+
+
def _create_png_bytes():
"""Create a small valid RGB PNG image."""
from PIL import Image
diff --git a/sdks/python/apache_beam/ml/transforms/embeddings/huggingface.py
b/sdks/python/apache_beam/ml/transforms/embeddings/huggingface.py
index a6905c37545..c3eeccf2a0f 100644
--- a/sdks/python/apache_beam/ml/transforms/embeddings/huggingface.py
+++ b/sdks/python/apache_beam/ml/transforms/embeddings/huggingface.py
@@ -21,6 +21,7 @@ import os
from collections.abc import Callable
from collections.abc import Mapping
from collections.abc import Sequence
+from typing import TYPE_CHECKING
from typing import Any
from typing import Optional
@@ -33,14 +34,29 @@ from apache_beam.ml.transforms.base import EmbeddingsManager
from apache_beam.ml.transforms.base import _ImageEmbeddingHandler
from apache_beam.ml.transforms.base import _TextEmbeddingHandler
-try:
+# sentence-transformers is intentionally not imported at module import time.
+# It is only needed for loading the model when the pipeline is executed (for
+# example, on the workers) and should not be required for constructing and
+# submitting pipelines.
+if TYPE_CHECKING:
from sentence_transformers import SentenceTransformer
-except ImportError:
- SentenceTransformer = None
_LOGGER = logging.getLogger(__name__)
+def _import_sentence_transformer() -> Callable:
+ """Returns sentence_transformers.SentenceTransformer, importing it lazily."""
+ try:
+ from sentence_transformers import SentenceTransformer
+ except ImportError as e:
+ raise ImportError(
+ "sentence-transformers is required to load SentenceTransformer "
+ "models, e.g. for SentenceTransformerEmbeddings. Please install it "
+ "using `pip install sentence-transformers` in the environment where "
+ "the pipeline is executed (for example, on the workers).") from e
+ return SentenceTransformer
+
+
# TODO: https://github.com/apache/beam/issues/29621
# Use HuggingFaceModelHandlerTensor once the import issue is fixed.
# Right now, the hugging face model handler import torch and tensorflow
@@ -52,7 +68,7 @@ class _SentenceTransformerModelHandler(ModelHandler):
def __init__(
self,
model_name: str,
- model_class: Callable,
+ model_class: Optional[Callable] = None,
load_model_args: Optional[dict] = None,
min_batch_size: Optional[int] = None,
max_batch_size: Optional[int] = None,
@@ -61,6 +77,9 @@ class _SentenceTransformerModelHandler(ModelHandler):
**kwargs):
self._max_seq_length = max_seq_length
self.model_name = model_name
+ # If not set, sentence_transformers.SentenceTransformer is used. It is
+ # imported in load_model(), i.e. only when the pipeline is executed, so
that
+ # sentence-transformers is not required for job submission.
self._model_class = model_class
self._load_model_args = load_model_args
self._min_batch_size = min_batch_size
@@ -68,23 +87,25 @@ class _SentenceTransformerModelHandler(ModelHandler):
self._large_model = large_model
self._kwargs = kwargs
- if not SentenceTransformer:
- raise ImportError(
- "sentence-transformers is required to use "
- "SentenceTransformerEmbeddings."
- "Please install it with using `pip install sentence-transformers`.")
-
def run_inference(
self,
batch: Sequence[str],
- model: SentenceTransformer,
+ model: 'SentenceTransformer',
inference_args: Optional[dict[str, Any]] = None,
):
inference_args = inference_args or {}
return model.encode(batch, **inference_args)
def load_model(self):
- model = self._model_class(self.model_name, **self._load_model_args)
+ model_class = self._model_class
+ # Checking callable() instead of None also covers artifacts written by
+ # Beam 2.77.0 and earlier, which stored a reference to the
+ # SentenceTransformer class. jsonpickle restores such a reference as a
+ # non-callable placeholder if sentence-transformers was not installed when
+ # the artifacts were read (for example, during job submission).
+ if not callable(model_class):
+ model_class = _import_sentence_transformer()
+ model = model_class(self.model_name, **(self._load_model_args or {}))
if self._max_seq_length:
model.max_seq_length = self._max_seq_length
return model
@@ -139,8 +160,9 @@ class SentenceTransformerEmbeddings(EmbeddingsManager):
self.image_model = image_model
def get_model_handler(self):
+ # model_class is not set, so that sentence-transformers is only imported
+ # when the model is loaded during pipeline execution.
return _SentenceTransformerModelHandler(
- model_class=SentenceTransformer,
max_seq_length=self.max_seq_length,
model_name=self.model_name,
load_model_args=self.load_model_args,
diff --git
a/sdks/python/apache_beam/ml/transforms/embeddings/huggingface_test.py
b/sdks/python/apache_beam/ml/transforms/embeddings/huggingface_test.py
index 5e56dc71054..12990cb4c2c 100644
--- a/sdks/python/apache_beam/ml/transforms/embeddings/huggingface_test.py
+++ b/sdks/python/apache_beam/ml/transforms/embeddings/huggingface_test.py
@@ -14,12 +14,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import contextlib
+import importlib.util
import os
import shutil
+import sys
import tempfile
+import types
import unittest
import uuid
+import jsonpickle
import numpy as np
import pytest
from parameterized import parameterized
@@ -28,6 +33,8 @@ import apache_beam as beam
from apache_beam.ml.inference.base import RunInference
from apache_beam.ml.transforms import base
from apache_beam.ml.transforms.base import MLTransform
+from apache_beam.ml.transforms.embeddings import huggingface
+from apache_beam.testing import test_utils
from apache_beam.testing.util import assert_that
from apache_beam.testing.util import equal_to
@@ -330,6 +337,141 @@ class
SentenceTransformerEmbeddingsTest(unittest.TestCase):
embedding_config))
+class _FakeSentenceTransformer:
+ """A stand-in for sentence_transformers.SentenceTransformer."""
+ def __init__(self, model_name, **kwargs):
+ self.model_name = model_name
+ self.kwargs = kwargs
+ self.max_seq_length = None
+
+
[email protected]
+def _fake_sentence_transformers_module():
+ """Replaces the sentence_transformers module with a fake one."""
+ fake_module = types.ModuleType('sentence_transformers')
+ fake_module.SentenceTransformer = _FakeSentenceTransformer
+ # Also hides the real sentence_transformers modules, if already imported, and
+ # restores them afterwards.
+ with test_utils.block_imports('sentence_transformers'):
+ sys.modules['sentence_transformers'] = fake_module
+ try:
+ yield
+ finally:
+ del sys.modules['sentence_transformers']
+
+
+def _serialize_and_deserialize(pipeline):
+ """Serializes the pipeline, as done during job submission, and deserializes
+ it again. Deserializing fails if the pipeline references (i.e. requires) a
+ module that can't be imported."""
+ return beam.Pipeline.from_runner_api(
+ pipeline.to_runner_api(), pipeline.runner, pipeline.options)
+
+
+class SentenceTransformerJobSubmissionTest(unittest.TestCase):
+ """Tests that sentence-transformers is only used when executing pipelines.
+
+ sentence-transformers (and its heavy dependencies, like torch) should only be
+ needed for loading the model when the pipeline is executed (for example, on
+ the workers), but not for constructing and submitting the pipeline. These
+ tests don't require sentence-transformers to be installed.
+ """
+ def setUp(self):
+ self.artifact_location = tempfile.mkdtemp(prefix='sentence_transformers_')
+
+ def tearDown(self):
+ shutil.rmtree(self.artifact_location)
+
+ def _apply_ml_transform(self, pipeline, ml_transform):
+ _ = (
+ pipeline
+ | beam.Create([{
+ test_query_column: test_query
+ }])
+ | ml_transform)
+
+ def test_importing_module_does_not_import_sentence_transformers(self):
+ # Executes the module in a new module object, i.e. without replacing the
+ # already imported module.
+ spec = importlib.util.find_spec(huggingface.__name__)
+ with test_utils.block_imports('sentence_transformers') as
attempted_imports:
+ spec.loader.exec_module(importlib.util.module_from_spec(spec))
+ self.assertEqual(attempted_imports, [])
+
+ @parameterized.expand([('text_model', False), ('image_model', True)])
+ def test_job_submission_does_not_use_sentence_transformers(
+ self, unused_name, image_model):
+ with test_utils.block_imports('sentence_transformers') as
attempted_imports:
+ embedding_config = huggingface.SentenceTransformerEmbeddings(
+ model_name=DEFAULT_MODEL_NAME,
+ columns=[test_query_column],
+ image_model=image_model)
+ pipeline = beam.Pipeline()
+ self._apply_ml_transform(
+ pipeline,
+ MLTransform(
+ write_artifact_location=self.artifact_location).with_transform(
+ embedding_config))
+ _serialize_and_deserialize(pipeline)
+ self.assertEqual(attempted_imports, [])
+
+ def test_reading_artifacts_does_not_use_sentence_transformers(self):
+ with test_utils.block_imports('sentence_transformers') as
attempted_imports:
+ embedding_config = huggingface.SentenceTransformerEmbeddings(
+ model_name=DEFAULT_MODEL_NAME, columns=[test_query_column])
+ # Artifacts are written when constructing the pipeline.
+ self._apply_ml_transform(
+ beam.Pipeline(),
+ MLTransform(
+ write_artifact_location=self.artifact_location).with_transform(
+ embedding_config))
+ pipeline = beam.Pipeline()
+ self._apply_ml_transform(
+ pipeline, MLTransform(read_artifact_location=self.artifact_location))
+ _serialize_and_deserialize(pipeline)
+ self.assertEqual(attempted_imports, [])
+
+ def test_loading_model_requires_sentence_transformers(self):
+ model_handler = huggingface.SentenceTransformerEmbeddings(
+ model_name=DEFAULT_MODEL_NAME,
+ columns=[test_query_column]).get_model_handler()
+ with test_utils.block_imports('sentence_transformers'):
+ with self.assertRaisesRegex(ImportError,
+ 'sentence-transformers is required'):
+ model_handler.load_model()
+
+ def test_loading_model_uses_sentence_transformers(self):
+ model_handler = huggingface.SentenceTransformerEmbeddings(
+ model_name=DEFAULT_MODEL_NAME,
+ columns=[test_query_column],
+ max_seq_length=128,
+ load_model_args={
+ 'device': 'cpu'
+ }).get_model_handler()
+ with _fake_sentence_transformers_module():
+ model = model_handler.load_model()
+ self.assertIsInstance(model, _FakeSentenceTransformer)
+ self.assertEqual(model.model_name, DEFAULT_MODEL_NAME)
+ self.assertEqual(model.kwargs, {'device': 'cpu'})
+ self.assertEqual(model.max_seq_length, 128)
+
+ def test_loading_model_from_artifacts_written_by_older_versions(self):
+ model_handler = huggingface.SentenceTransformerEmbeddings(
+ model_name=DEFAULT_MODEL_NAME,
+ columns=[test_query_column]).get_model_handler()
+ # Beam 2.77.0 and earlier stored a reference to the SentenceTransformer
+ # class in the artifacts. This is how jsonpickle restores the reference if
+ # sentence-transformers is not installed when reading the artifacts.
+ with test_utils.block_imports('sentence_transformers'):
+ model_handler._model_class = jsonpickle.decode(
+ '{"py/type": '
+ '"sentence_transformers.SentenceTransformer.SentenceTransformer"}')
+ with _fake_sentence_transformers_module():
+ model = model_handler.load_model()
+ self.assertIsInstance(model, _FakeSentenceTransformer)
+ self.assertEqual(model.model_name, DEFAULT_MODEL_NAME)
+
+
@unittest.skipIf(not _HF_TOKEN, 'HF_TOKEN environment variable not set.')
class HuggingfaceInferenceAPITest(unittest.TestCase):
def setUp(self):
diff --git a/sdks/python/apache_beam/testing/test_utils.py
b/sdks/python/apache_beam/testing/test_utils.py
index e31c53765b8..425b4c6dfac 100644
--- a/sdks/python/apache_beam/testing/test_utils.py
+++ b/sdks/python/apache_beam/testing/test_utils.py
@@ -22,11 +22,14 @@ For internal use only; no backwards-compatibility
guarantees.
# pytype: skip-file
+import contextlib
import hashlib
import importlib
+import importlib.abc
import os
import shutil
import struct
+import sys
import tempfile
import numpy as np
@@ -125,6 +128,64 @@ def patch_retry(testcase, module):
testcase.addCleanup(remove_patches)
+class _ImportBlocker(importlib.abc.MetaPathFinder):
+ """A meta path finder that blocks, and records, imports of given packages."""
+ def __init__(self, packages):
+ self._packages = tuple(packages)
+ self.attempted_imports = []
+
+ def blocks(self, module_name):
+ return any(
+ module_name == package or module_name.startswith(package + '.')
+ for package in self._packages)
+
+ def find_spec(self, fullname, path, target=None):
+ if self.blocks(fullname):
+ self.attempted_imports.append(fullname)
+ raise ModuleNotFoundError(
+ 'Import of %s is blocked for testing.' % fullname, name=fullname)
+ return None
+
+
[email protected]
+def block_imports(*packages):
+ """Context manager that makes the given packages unimportable.
+
+ Within the context, importing any of the given packages, or their
+ submodules, raises a ModuleNotFoundError as if they were not installed. This
+ also applies to packages that were already imported. Such packages are
+ restored when exiting the context.
+
+ This is useful, for example, for verifying that constructing and submitting
+ a pipeline neither requires, nor tries to import, a dependency that is only
+ needed when executing the pipeline on workers::
+
+ with block_imports('sentence_transformers') as attempted_imports:
+ ... # Construct and serialize the pipeline.
+ assert not attempted_imports
+
+ Args:
+ *packages: names of the top-level packages, or modules, to block.
+
+ Yields:
+ A list with the names of all modules whose import was attempted, and
+ blocked, within the context.
+ """
+ blocker = _ImportBlocker(packages)
+ hidden_modules = {
+ name: module
+ for name, module in list(sys.modules.items()) if blocker.blocks(name)
+ }
+ for name in hidden_modules:
+ del sys.modules[name]
+ sys.meta_path.insert(0, blocker)
+ try:
+ yield blocker.attempted_imports
+ finally:
+ sys.meta_path.remove(blocker)
+ sys.modules.update(hidden_modules)
+
+
@retry.with_exponential_backoff(
num_retries=3, retry_filter=retry.retry_on_beam_io_error_filter)
def delete_files(file_paths):
diff --git a/sdks/python/apache_beam/testing/test_utils_test.py
b/sdks/python/apache_beam/testing/test_utils_test.py
index 1a89ada2a70..9c379cf35aa 100644
--- a/sdks/python/apache_beam/testing/test_utils_test.py
+++ b/sdks/python/apache_beam/testing/test_utils_test.py
@@ -19,8 +19,10 @@
# pytype: skip-file
+import importlib
import logging
import os
+import sys
import tempfile
import unittest
@@ -95,6 +97,35 @@ class TestUtilsTest(unittest.TestCase):
pub_client.delete_topic.assert_called_with(topic=topic.name)
+class BlockImportsTest(unittest.TestCase):
+ def test_block_imports_blocks_and_records_imports(self):
+ with utils.block_imports('colorsys') as attempted_imports:
+ with self.assertRaises(ImportError):
+ importlib.import_module('colorsys')
+ self.assertEqual(attempted_imports, ['colorsys'])
+ # The module can be imported again after exiting the context.
+ importlib.import_module('colorsys')
+
+ def test_block_imports_hides_and_restores_imported_modules(self):
+ wsgiref_util = importlib.import_module('wsgiref.util')
+ wsgiref = sys.modules['wsgiref']
+ with utils.block_imports('wsgiref') as attempted_imports:
+ self.assertNotIn('wsgiref', sys.modules)
+ self.assertNotIn('wsgiref.util', sys.modules)
+ with self.assertRaises(ImportError):
+ importlib.import_module('wsgiref.util')
+ # Importing a submodule first imports its parent package.
+ self.assertEqual(attempted_imports, ['wsgiref'])
+ self.assertIs(sys.modules['wsgiref'], wsgiref)
+ self.assertIs(sys.modules['wsgiref.util'], wsgiref_util)
+
+ def test_block_imports_only_blocks_given_packages(self):
+ with utils.block_imports('wsgi', 'colorsys.foo') as attempted_imports:
+ importlib.import_module('wsgiref')
+ importlib.import_module('colorsys')
+ self.assertEqual(attempted_imports, [])
+
+
class LCGeneratorTest(unittest.TestCase):
Generator = utils.LCGenerator
diff --git a/sdks/python/apache_beam/yaml/yaml_ml_test.py
b/sdks/python/apache_beam/yaml/yaml_ml_test.py
index d8b1bdbae1b..c4fc179b9a3 100644
--- a/sdks/python/apache_beam/yaml/yaml_ml_test.py
+++ b/sdks/python/apache_beam/yaml/yaml_ml_test.py
@@ -20,6 +20,7 @@ import tempfile
import unittest
import apache_beam as beam
+from apache_beam.testing import test_utils
from apache_beam.testing.util import assert_that
from apache_beam.testing.util import equal_to
from apache_beam.yaml.yaml_transform import YamlTransform
@@ -28,7 +29,7 @@ try:
# pylint: disable=wrong-import-order, wrong-import-position, unused-import
from apache_beam.ml.transforms import tft
except ImportError:
- raise unittest.SkipTest('tensorflow_transform is not installed.')
+ tft = None
TRAIN_DATA = [
beam.Row(num=0, text='And God said, Let there be light,'),
@@ -41,6 +42,7 @@ TEST_DATA = [
]
[email protected](tft is None, 'tensorflow_transform is not installed.')
class MLTransformTest(unittest.TestCase):
def test_ml_transform(self):
ml_opts = beam.options.pipeline_options.PipelineOptions(
@@ -236,6 +238,42 @@ class MLTransformTest(unittest.TestCase):
assert_that(result | beam.Map(check_row), equal_to([1, 2, 3]))
+class MLTransformJobSubmissionTest(unittest.TestCase):
+ def test_sentence_transformer_embedding_does_not_use_sentence_transformers(
+ self):
+ # sentence-transformers should only be needed when executing the pipeline
+ # (for example, on the workers), not for constructing and submitting it.
+ ml_opts = beam.options.pipeline_options.PipelineOptions(
+ pickle_library='cloudpickle', yaml_experimental_features=['ML'])
+ with tempfile.TemporaryDirectory() as tempdir:
+ with test_utils.block_imports(
+ 'sentence_transformers') as attempted_imports:
+ pipeline = beam.Pipeline(options=ml_opts)
+ _ = pipeline | YamlTransform(
+ f'''
+ type: chain
+ transforms:
+ - type: Create
+ config:
+ elements:
+ - {{id: 1, log_message: "Error in module A"}}
+ - type: MLTransform
+ config:
+ write_artifact_location: {tempdir}
+ transforms:
+ - type: SentenceTransformerEmbeddings
+ config:
+ model_name: all-MiniLM-L6-v2
+ columns: [log_message]
+ ''')
+ # Serializes the pipeline, as done during job submission, and
+ # deserializes it again. Deserializing fails if the pipeline references
+ # (i.e. requires) a module that can't be imported.
+ beam.Pipeline.from_runner_api(
+ pipeline.to_runner_api(), pipeline.runner, pipeline.options)
+ self.assertEqual(attempted_imports, [])
+
+
if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO)
unittest.main()