This is an automated email from the ASF dual-hosted git repository.

shunping 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 0e32466f7c9 [Python] Make cloud storage dependencies optional on 
import in gcsio and s3io (#40178)
0e32466f7c9 is described below

commit 0e32466f7c99d9b0533d5deee52a25a045914447
Author: Shunping Huang <[email protected]>
AuthorDate: Fri Sep 18 15:17:34 2026 -0400

    [Python] Make cloud storage dependencies optional on import in gcsio and 
s3io (#40178)
    
    * Add tests to reproduce first
    
    * Make GCP storage dependencies optional on import in gcsio
    
    * Fix BOTO3_INSTALLED detection in s3io when boto3 is missing
    
    * Reformat
    
    * Fix broken tests.
---
 .../examples/cookbook/group_with_coder_test.py     |  2 ++
 .../transforms/elementwise/runinference_test.py    |  3 ++
 sdks/python/apache_beam/io/aws/s3io.py             |  3 +-
 .../io/external/xlang_bigqueryio_it_test.py        |  3 ++
 .../io/external/xlang_snowflakeio_it_test.py       |  3 ++
 sdks/python/apache_beam/io/filesystems_test.py     | 22 ++++++++++++
 .../io/gcp/gcsfilesystem_integration_test.py       |  3 +-
 sdks/python/apache_beam/io/gcp/gcsio.py            | 41 +++++++++++++++-------
 .../apache_beam/io/gcp/gcsio_integration_test.py   |  2 ++
 .../ml/inference/onnx_inference_test.py            |  3 ++
 .../ml/inference/pytorch_inference_test.py         |  3 ++
 .../ml/inference/sklearn_inference_it_test.py      |  3 ++
 .../ml/inference/tensorrt_inference_test.py        |  3 ++
 .../python/apache_beam/options/pipeline_options.py | 10 ++++++
 .../apache_beam/options/pipeline_options_test.py   |  4 +--
 sdks/python/apache_beam/runners/render.py          |  2 ++
 16 files changed, 94 insertions(+), 16 deletions(-)

diff --git a/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py 
b/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py
index 9cf36e70e45..90b4829906a 100644
--- a/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py
+++ b/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py
@@ -32,6 +32,8 @@ from apache_beam.testing.test_utils import 
read_files_from_pattern
 # Protect against environments where gcsio library is not available.
 try:
   from apache_beam.io.gcp import gcsio
+  if not gcsio.GCS_INSTALLED:
+    gcsio = None
 except ImportError:
   gcsio = None
 
diff --git 
a/sdks/python/apache_beam/examples/snippets/transforms/elementwise/runinference_test.py
 
b/sdks/python/apache_beam/examples/snippets/transforms/elementwise/runinference_test.py
index 8dd46c659f3..c65e54ca54e 100644
--- 
a/sdks/python/apache_beam/examples/snippets/transforms/elementwise/runinference_test.py
+++ 
b/sdks/python/apache_beam/examples/snippets/transforms/elementwise/runinference_test.py
@@ -39,7 +39,10 @@ except ImportError:
 
 # pylint: disable=wrong-import-order, wrong-import-position, 
ungrouped-imports, unused-import
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
+  if not GCS_INSTALLED:
+    raise unittest.SkipTest('GCP dependencies are not installed')
 except ImportError:
   raise unittest.SkipTest('GCP dependencies are not installed')
 
diff --git a/sdks/python/apache_beam/io/aws/s3io.py 
b/sdks/python/apache_beam/io/aws/s3io.py
index 534e63eab51..69cc9f9989e 100644
--- a/sdks/python/apache_beam/io/aws/s3io.py
+++ b/sdks/python/apache_beam/io/aws/s3io.py
@@ -36,7 +36,8 @@ from apache_beam.utils import retry
 
 try:
   # pylint: disable=wrong-import-order, wrong-import-position
-  # pylint: disable=ungrouped-imports
+  # pylint: disable=ungrouped-imports, unused-import
+  import boto3
   from apache_beam.io.aws.clients.s3 import boto3_client
   BOTO3_INSTALLED = True
 except ImportError:
diff --git a/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py 
b/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py
index 49725d54e99..c7b8568595f 100644
--- a/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py
+++ b/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py
@@ -44,7 +44,10 @@ from apache_beam.utils.timestamp import Timestamp
 # pylint: disable=wrong-import-order, wrong-import-position
 
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsio import GcsIO
+  if not GCS_INSTALLED:
+    GcsIO = None
 except ImportError:
   GcsIO = None
 
diff --git a/sdks/python/apache_beam/io/external/xlang_snowflakeio_it_test.py 
b/sdks/python/apache_beam/io/external/xlang_snowflakeio_it_test.py
index f78175a8696..110d385c265 100644
--- a/sdks/python/apache_beam/io/external/xlang_snowflakeio_it_test.py
+++ b/sdks/python/apache_beam/io/external/xlang_snowflakeio_it_test.py
@@ -61,7 +61,10 @@ from apache_beam.testing.util import equal_to
 
 # pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
+  if not GCS_INSTALLED:
+    GCSFileSystem = None
 except ImportError:
   GCSFileSystem = None
 # pylint: enable=wrong-import-order, wrong-import-position, ungrouped-imports
diff --git a/sdks/python/apache_beam/io/filesystems_test.py 
b/sdks/python/apache_beam/io/filesystems_test.py
index 1ea7c34d9f4..e4818d9a935 100644
--- a/sdks/python/apache_beam/io/filesystems_test.py
+++ b/sdks/python/apache_beam/io/filesystems_test.py
@@ -30,8 +30,12 @@ import unittest
 import mock
 
 from apache_beam.io import localfilesystem
+from apache_beam.io.aws import s3filesystem
+from apache_beam.io.aws import s3io
 from apache_beam.io.filesystem import BeamIOError
 from apache_beam.io.filesystems import FileSystems
+from apache_beam.io.gcp import gcsfilesystem
+from apache_beam.io.gcp import gcsio
 
 
 def _gen_fake_join(separator):
@@ -63,9 +67,27 @@ class FileSystemsTest(unittest.TestCase):
         isinstance(
             FileSystems.get_filesystem('c:\\abc\\def'),
             localfilesystem.LocalFileSystem))
+    self.assertTrue(
+        isinstance(
+            FileSystems.get_filesystem('gs://bucket/file'),
+            gcsfilesystem.GCSFileSystem))
+    self.assertTrue(
+        isinstance(
+            FileSystems.get_filesystem('s3://bucket/file'),
+            s3filesystem.S3FileSystem))
     with self.assertRaises(ValueError):
       FileSystems.get_filesystem('error://abc/def')
 
+  def test_missing_cloud_dependencies_raise_runtime_error(self):
+    with mock.patch.object(gcsio, 'GCS_INSTALLED', False):
+      with self.assertRaisesRegex(RuntimeError,
+                                  'GCP dependencies are not installed'):
+        gcsio.GcsIO()
+    with mock.patch.object(s3io, 'BOTO3_INSTALLED', False):
+      with self.assertRaisesRegex(RuntimeError,
+                                  'AWS dependencies are not installed'):
+        s3io.S3IO(options={})
+
   @mock.patch('apache_beam.io.localfilesystem.os')
   def test_unix_path_join(self, *unused_mocks):
     # Test joining of Unix paths.
diff --git a/sdks/python/apache_beam/io/gcp/gcsfilesystem_integration_test.py 
b/sdks/python/apache_beam/io/gcp/gcsfilesystem_integration_test.py
index 3cefb6ca30d..824c11169db 100644
--- a/sdks/python/apache_beam/io/gcp/gcsfilesystem_integration_test.py
+++ b/sdks/python/apache_beam/io/gcp/gcsfilesystem_integration_test.py
@@ -39,8 +39,9 @@ from apache_beam.io.filesystems import FileSystems
 from apache_beam.testing.test_pipeline import TestPipeline
 
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
-  fs_not_available = False
+  fs_not_available = not GCS_INSTALLED
 except ImportError:
   fs_not_available = True  # type: ignore
 
diff --git a/sdks/python/apache_beam/io/gcp/gcsio.py 
b/sdks/python/apache_beam/io/gcp/gcsio.py
index 179525de2cb..fc9a0cc1fd4 100644
--- a/sdks/python/apache_beam/io/gcp/gcsio.py
+++ b/sdks/python/apache_beam/io/gcp/gcsio.py
@@ -35,22 +35,33 @@ import time
 from typing import Optional
 from typing import Union
 
-from google.api_core.exceptions import Conflict
-from google.api_core.exceptions import RetryError
-from google.cloud import storage
-from google.cloud.exceptions import NotFound
-from google.cloud.exceptions import from_http_response
-from google.cloud.storage.fileio import BlobReader
-from google.cloud.storage.fileio import BlobWriter
-from google.cloud.storage.retry import DEFAULT_RETRY
-
 from apache_beam import version as beam_version
 from apache_beam.internal.gcp import auth
-from apache_beam.io.gcp import gcsio_retry
 from apache_beam.metrics.metric import Metrics
 from apache_beam.options.pipeline_options import GoogleCloudOptions
 from apache_beam.options.pipeline_options import PipelineOptions
 
+try:
+  # pylint: disable=wrong-import-order, wrong-import-position
+  # pylint: disable=ungrouped-imports
+  from google.api_core.exceptions import Conflict
+  from google.api_core.exceptions import RetryError
+  from google.cloud import storage
+  from google.cloud.exceptions import NotFound
+  from google.cloud.exceptions import from_http_response
+  from google.cloud.storage.fileio import BlobReader
+  from google.cloud.storage.fileio import BlobWriter
+  from google.cloud.storage.retry import DEFAULT_RETRY
+
+  from apache_beam.io.gcp import gcsio_retry
+  GCS_INSTALLED = True
+except ImportError:
+  GCS_INSTALLED = False
+  storage = None  # type: ignore
+  BlobReader = object  # type: ignore
+  BlobWriter = object  # type: ignore
+  DEFAULT_RETRY = None  # type: ignore
+
 __all__ = ['GcsIO', 'create_storage_client']
 
 _LOGGER = logging.getLogger(__name__)
@@ -202,13 +213,18 @@ class GcsIO(object):
   """Google Cloud Storage I/O client."""
   def __init__(
       self,
-      storage_client: Optional[storage.Client] = None,
+      storage_client: Optional['storage.Client'] = None,
       pipeline_options: Optional[Union[dict, PipelineOptions]] = None) -> None:
     if pipeline_options is None:
       pipeline_options = PipelineOptions()
     elif isinstance(pipeline_options, dict):
       pipeline_options = PipelineOptions.from_dictionary(pipeline_options)
     if storage_client is None:
+      if not GCS_INSTALLED:
+        message = (
+            'GCP dependencies are not installed, and no alternative '
+            'client was provided to GcsIO.')
+        raise RuntimeError(message)
       storage_client = create_storage_client(pipeline_options)
 
     google_cloud_options = pipeline_options.view_as(GoogleCloudOptions)
@@ -220,7 +236,8 @@ class GcsIO(object):
     self.client = storage_client
     self._rewrite_cb = None
     self.bucket_to_project_number = {}
-    self._storage_client_retry = gcsio_retry.get_retry(pipeline_options)
+    self._storage_client_retry = (
+        gcsio_retry.get_retry(pipeline_options) if GCS_INSTALLED else None)
     self._use_blob_generation = getattr(
         google_cloud_options, 'enable_gcsio_blob_generation', False)
 
diff --git a/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py 
b/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py
index a68b1179faf..b86fb4e8545 100644
--- a/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py
+++ b/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py
@@ -52,6 +52,8 @@ from apache_beam.transforms.combiners import Count
 
 try:
   from apache_beam.io.gcp import gcsio
+  if not gcsio.GCS_INSTALLED:
+    gcsio = None  # type: ignore
 except ImportError:
   gcsio = None  # type: ignore
 
diff --git a/sdks/python/apache_beam/ml/inference/onnx_inference_test.py 
b/sdks/python/apache_beam/ml/inference/onnx_inference_test.py
index 61e8c983c9d..37d667c3a9e 100644
--- a/sdks/python/apache_beam/ml/inference/onnx_inference_test.py
+++ b/sdks/python/apache_beam/ml/inference/onnx_inference_test.py
@@ -55,7 +55,10 @@ except ImportError:
   raise unittest.SkipTest('Onnx dependencies are not installed')
 
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
+  if not GCS_INSTALLED:
+    GCSFileSystem = None  # type: ignore
 except ImportError:
   GCSFileSystem = None  # type: ignore
 
diff --git a/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py 
b/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py
index 8efec14c865..2a3e23f6807 100644
--- a/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py
+++ b/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py
@@ -49,7 +49,10 @@ except ImportError:
   raise unittest.SkipTest('PyTorch dependencies are not installed')
 
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
+  if not GCS_INSTALLED:
+    GCSFileSystem = None  # type: ignore
 except ImportError:
   GCSFileSystem = None  # type: ignore
 
diff --git a/sdks/python/apache_beam/ml/inference/sklearn_inference_it_test.py 
b/sdks/python/apache_beam/ml/inference/sklearn_inference_it_test.py
index c82b1698981..7ad94884b1b 100644
--- a/sdks/python/apache_beam/ml/inference/sklearn_inference_it_test.py
+++ b/sdks/python/apache_beam/ml/inference/sklearn_inference_it_test.py
@@ -32,7 +32,10 @@ from apache_beam.testing.test_pipeline import TestPipeline
 
 # pylint: disable=wrong-import-order, wrong-import-position, 
ungrouped-imports, unused-import
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
+  if not GCS_INSTALLED:
+    raise unittest.SkipTest('GCP dependencies are not installed')
 except ImportError:
   raise unittest.SkipTest('GCP dependencies are not installed')
 
diff --git a/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py 
b/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py
index 23b516f502a..ea2b9e4f8d4 100644
--- a/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py
+++ b/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py
@@ -45,7 +45,10 @@ except ImportError:
   raise unittest.SkipTest('TensorRT dependencies are not installed')
 
 try:
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
   from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
+  if not GCS_INSTALLED:
+    GCSFileSystem = None  # type: ignore
 except ImportError:
   GCSFileSystem = None  # type: ignore
 
diff --git a/sdks/python/apache_beam/options/pipeline_options.py 
b/sdks/python/apache_beam/options/pipeline_options.py
index 9c460f7678e..98e3dea38d2 100644
--- a/sdks/python/apache_beam/options/pipeline_options.py
+++ b/sdks/python/apache_beam/options/pipeline_options.py
@@ -1212,6 +1212,11 @@ class GoogleCloudOptions(PipelineOptions):
     except ImportError:
       _LOGGER.warning('Unable to create default GCS bucket.')
       return None
+    if not gcsio.GCS_INSTALLED:
+      _LOGGER.warning(
+          'Unable to create default GCS bucket because GCP dependencies are '
+          'not installed.')
+      return None
     bucket = gcsio.get_or_create_default_gcs_bucket(self)
     if bucket:
       return 'gs://%s/' % bucket.id
@@ -1229,6 +1234,11 @@ class GoogleCloudOptions(PipelineOptions):
     gcs_path = getattr(self, arg_name, None)
     try:
       from apache_beam.io.gcp import gcsio
+      if not gcsio.GCS_INSTALLED:
+        _LOGGER.warning(
+            'Unable to check soft delete policy because GCP dependencies are '
+            'not installed.')
+        return
       if gcsio.GcsIO().is_soft_delete_enabled(gcs_path):
         logger.log_first_n(
             logging.WARN,
diff --git a/sdks/python/apache_beam/options/pipeline_options_test.py 
b/sdks/python/apache_beam/options/pipeline_options_test.py
index fbdaf25f0e8..d9e1d850ec7 100644
--- a/sdks/python/apache_beam/options/pipeline_options_test.py
+++ b/sdks/python/apache_beam/options/pipeline_options_test.py
@@ -47,8 +47,8 @@ from apache_beam.transforms.display_test import 
DisplayDataItemMatcher
 _LOGGER = logging.getLogger(__name__)
 
 try:
-  import apache_beam.io.gcp.gcsio  # pylint: disable=unused-import
-  has_gcsio = True
+  from apache_beam.io.gcp.gcsio import GCS_INSTALLED
+  has_gcsio = GCS_INSTALLED
 except ImportError:
   has_gcsio = False
 
diff --git a/sdks/python/apache_beam/runners/render.py 
b/sdks/python/apache_beam/runners/render.py
index 9f37e0201d9..17fd922782e 100644
--- a/sdks/python/apache_beam/runners/render.py
+++ b/sdks/python/apache_beam/runners/render.py
@@ -75,6 +75,8 @@ from apache_beam.runners.portability.fn_api_runner import 
translations
 
 try:
   from apache_beam.io.gcp import gcsio
+  if not gcsio.GCS_INSTALLED:
+    gcsio = None  # type: ignore
 except ImportError:
   gcsio = None  # type: ignore
 

Reply via email to