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 1f76c9c36d6 Fix Python unit tests broken by google-api-core arriving 
via cloud-sql-python-connector 1.23.0 (#40623)
1f76c9c36d6 is described below

commit 1f76c9c36d642f58ca81b6b1fb757411771323cb
Author: Shunping Huang <[email protected]>
AuthorDate: Fri Oct 16 10:45:22 2026 -0400

    Fix Python unit tests broken by google-api-core arriving via 
cloud-sql-python-connector 1.23.0 (#40623)
---
 .../beam_PostCommit_Python_Versions.json           |  2 +-
 sdks/python/apache_beam/dataframe/io_it_test.py    |  7 ++--
 sdks/python/apache_beam/dataframe/io_test.py       | 11 ++-----
 .../apache_beam/io/requestresponse_it_test.py      |  5 +++
 sdks/python/apache_beam/testing/test_utils.py      | 25 ++++++++++++++
 sdks/python/apache_beam/testing/test_utils_test.py | 37 +++++++++++++++++++++
 .../enrichment_handlers/cloudsql_it_test.py        |  3 ++
 sdks/python/conftest.py                            | 38 +++++++++++++++++-----
 8 files changed, 106 insertions(+), 22 deletions(-)

diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json 
b/.github/trigger_files/beam_PostCommit_Python_Versions.json
index 8b2c8c445c1..b5704c67ef1 100644
--- a/.github/trigger_files/beam_PostCommit_Python_Versions.json
+++ b/.github/trigger_files/beam_PostCommit_Python_Versions.json
@@ -1,4 +1,4 @@
 {
   "comment": "Modify this file in a trivial way to cause this test suite to 
run",
-  "revision": 5
+  "revision": 6
 }
diff --git a/sdks/python/apache_beam/dataframe/io_it_test.py 
b/sdks/python/apache_beam/dataframe/io_it_test.py
index da88ffb5476..9f750e2ff58 100644
--- a/sdks/python/apache_beam/dataframe/io_it_test.py
+++ b/sdks/python/apache_beam/dataframe/io_it_test.py
@@ -33,13 +33,12 @@ from apache_beam.testing.util import equal_to
 _LOGGER = logging.getLogger(__name__)
 
 try:
-  from google.api_core.exceptions import GoogleAPICallError
+  from apitools.base.py.exceptions import HttpError
 except ImportError:
-  GoogleAPICallError = None
+  HttpError = None
 
 
[email protected](
-    GoogleAPICallError is None, 'GCP dependencies are not installed')
[email protected](HttpError is None, 'GCP dependencies are not installed')
 class ReadUsingReadGbqTests(unittest.TestCase):
   @pytest.mark.it_postcommit
   def test_ReadGbq(self):
diff --git a/sdks/python/apache_beam/dataframe/io_test.py 
b/sdks/python/apache_beam/dataframe/io_test.py
index 051a85b379f..d9723e1e5bb 100644
--- a/sdks/python/apache_beam/dataframe/io_test.py
+++ b/sdks/python/apache_beam/dataframe/io_test.py
@@ -46,9 +46,9 @@ from apache_beam.testing.util import assert_that
 from apache_beam.testing.util import equal_to
 
 try:
-  from google.api_core.exceptions import GoogleAPICallError
+  from apitools.base.py.exceptions import HttpError
 except ImportError:
-  GoogleAPICallError = None
+  HttpError = None
 
 # Get major, minor version
 PD_VERSION = tuple(map(int, pd.__version__.split('.')[0:2]))
@@ -492,15 +492,10 @@ X     , c1, c2
                           set(self.read_all_lines(output + 'out2.csv*')))
 
 
[email protected](
-    GoogleAPICallError is None, 'GCP dependencies are not installed')
[email protected](HttpError is None, 'GCP dependencies are not installed')
 class ReadGbqTransformTests(unittest.TestCase):
   @mock.patch.object(BigQueryWrapper, 'get_table')
   def test_bad_schema_public_api_direct_read(self, get_table):
-    try:
-      bigquery.TableFieldSchema
-    except AttributeError:
-      raise ValueError('Please install GCP Dependencies.')
     fields = [
         bigquery.TableFieldSchema(name='stn', type='DOUBLE', mode="NULLABLE"),
         bigquery.TableFieldSchema(name='temp', type='FLOAT64', 
mode="REPEATED"),
diff --git a/sdks/python/apache_beam/io/requestresponse_it_test.py 
b/sdks/python/apache_beam/io/requestresponse_it_test.py
index 072a1dc27b9..4e4c2b63844 100644
--- a/sdks/python/apache_beam/io/requestresponse_it_test.py
+++ b/sdks/python/apache_beam/io/requestresponse_it_test.py
@@ -30,6 +30,7 @@ import apache_beam as beam
 from apache_beam.coders import coders
 from apache_beam.options.pipeline_options import PipelineOptions
 from apache_beam.testing.test_pipeline import TestPipeline
+from apache_beam.testing.test_utils import require_linux_docker
 
 # pylint: disable=ungrouped-imports
 try:
@@ -206,6 +207,10 @@ class FakeCallerForCache(Caller[str, str]):
 
 @pytest.mark.uses_testcontainer
 class TestRedisCache(unittest.TestCase):
+  @classmethod
+  def setUpClass(cls) -> None:
+    require_linux_docker()
+
   def setUp(self) -> None:
     self.retries = 5
     self._start_container()
diff --git a/sdks/python/apache_beam/testing/test_utils.py 
b/sdks/python/apache_beam/testing/test_utils.py
index 425b4c6dfac..ee953a902a0 100644
--- a/sdks/python/apache_beam/testing/test_utils.py
+++ b/sdks/python/apache_beam/testing/test_utils.py
@@ -31,6 +31,7 @@ import shutil
 import struct
 import sys
 import tempfile
+from unittest import SkipTest
 
 import numpy as np
 
@@ -186,6 +187,30 @@ def block_imports(*packages):
     sys.modules.update(hidden_modules)
 
 
+def require_linux_docker():
+  """Raises ``unittest.SkipTest`` unless Docker can run Linux containers.
+
+  Intended for tests that start containers via testcontainers, e.g. from
+  ``setUpClass``. Such tests need a reachable Docker daemon running Linux
+  containers. Some environments have no daemon at all (e.g. GitHub macOS
+  runners), while others only run Windows containers (e.g. GitHub Windows
+  runners).
+  """
+  try:
+    import docker
+    client = docker.from_env()
+    try:
+      os_type = client.info().get('OSType')
+    finally:
+      client.close()
+  except Exception as e:  # pylint: disable=broad-except
+    raise SkipTest(f'Docker is not available: {e}')
+  if os_type != 'linux':
+    raise SkipTest(
+        f'Docker daemon runs {os_type} containers; '
+        'Linux containers are required.')
+
+
 @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 9c379cf35aa..3b99687c115 100644
--- a/sdks/python/apache_beam/testing/test_utils_test.py
+++ b/sdks/python/apache_beam/testing/test_utils_test.py
@@ -126,6 +126,43 @@ class BlockImportsTest(unittest.TestCase):
     self.assertEqual(attempted_imports, [])
 
 
+class RequireLinuxDockerTest(unittest.TestCase):
+  def _fake_docker(self, os_type=None, error=None):
+    fake_docker = mock.MagicMock()
+    client = fake_docker.from_env.return_value
+    if error:
+      client.info.side_effect = error
+    else:
+      client.info.return_value = {'OSType': os_type}
+    return fake_docker, client
+
+  def test_linux_docker_does_not_skip(self):
+    fake_docker, client = self._fake_docker(os_type='linux')
+    with mock.patch.dict(sys.modules, {'docker': fake_docker}):
+      utils.require_linux_docker()
+    client.close.assert_called_once()
+
+  def test_windows_docker_skips(self):
+    fake_docker, client = self._fake_docker(os_type='windows')
+    with mock.patch.dict(sys.modules, {'docker': fake_docker}):
+      with self.assertRaisesRegex(unittest.SkipTest, 'windows containers'):
+        utils.require_linux_docker()
+    client.close.assert_called_once()
+
+  def test_unreachable_docker_skips(self):
+    fake_docker, client = self._fake_docker(
+        error=ConnectionError('no such file'))
+    with mock.patch.dict(sys.modules, {'docker': fake_docker}):
+      with self.assertRaisesRegex(unittest.SkipTest, 'Docker is not 
available'):
+        utils.require_linux_docker()
+    client.close.assert_called_once()
+
+  def test_missing_docker_sdk_skips(self):
+    with utils.block_imports('docker'):
+      with self.assertRaisesRegex(unittest.SkipTest, 'Docker is not 
available'):
+        utils.require_linux_docker()
+
+
 class LCGeneratorTest(unittest.TestCase):
   Generator = utils.LCGenerator
 
diff --git 
a/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql_it_test.py 
b/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql_it_test.py
index 04db85a75c2..644dd56978b 100644
--- a/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql_it_test.py
+++ b/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql_it_test.py
@@ -28,6 +28,7 @@ import pytest
 import apache_beam as beam
 from apache_beam.coders import coders
 from apache_beam.testing.test_pipeline import TestPipeline
+from apache_beam.testing.test_utils import require_linux_docker
 from apache_beam.testing.util import assert_that
 from apache_beam.testing.util import equal_to
 
@@ -589,6 +590,8 @@ class BaseExternalSQLDBEnrichment(BaseTestSQLEnrichment):
       # Skip setup for the base class.
       raise unittest.SkipTest("Base class - no db_adapter defined")
 
+    require_linux_docker()
+
     # Type hint data from subclasses.
     cls._db_adapter: DatabaseTypeAdapter
 
diff --git a/sdks/python/conftest.py b/sdks/python/conftest.py
index 92d90769fb1..0b2db2649a9 100644
--- a/sdks/python/conftest.py
+++ b/sdks/python/conftest.py
@@ -18,11 +18,11 @@
 """Pytest configuration and custom hooks."""
 
 import gc
+import math
 import os
 import sys
 import threading
 import time
-from types import SimpleNamespace
 
 import pytest
 
@@ -150,6 +150,32 @@ def enhance_mock_stability():
   time.sleep(0.05)
 
 
+def _configure_testcontainers_timeouts():
+  """Applies TC_* environment settings to testcontainers' global config.
+
+  testcontainers 4.x computes its wait timeout as ``max_tries * sleep_time``
+  from ``testcontainers_config``. It reads ``TC_MAX_TRIES`` and
+  ``TC_POOLING_INTERVAL`` itself, but not ``TC_TIMEOUT`` or ``TC_SLEEP_TIME``,
+  which Beam's CI workflows set. Translate them here so that the effective
+  timeout is at least ``TC_TIMEOUT`` seconds.
+  """
+  from testcontainers.core.config import testcontainers_config
+
+  sleep_time = float(
+      os.getenv("TC_SLEEP_TIME") or os.getenv("TC_POOLING_INTERVAL") or "1")
+  timeout = float(os.getenv("TC_TIMEOUT", "120"))
+  max_tries = int(os.getenv("TC_MAX_TRIES", "0"))
+
+  testcontainers_config.sleep_time = sleep_time
+  testcontainers_config.max_tries = max(
+      max_tries, math.ceil(timeout / sleep_time))
+  print(
+      "Set testcontainers max_tries=%d, sleep_time=%s (timeout=%ss)" % (
+          testcontainers_config.max_tries,
+          testcontainers_config.sleep_time,
+          testcontainers_config.timeout))
+
+
 def pytest_configure(config):
   """Saves options added in pytest_addoption for later use.
   This is necessary since pytest-xdist workers do not have the
@@ -159,14 +185,8 @@ def pytest_configure(config):
   # for the entire test session.
   print("\n--- Applying global testcontainers timeout configuration ---")
   try:
-    from testcontainers.core import waiting_utils
-    waiting_utils.config = SimpleNamespace(
-        timeout=int(os.getenv("TC_TIMEOUT", "120")),
-        max_tries=int(os.getenv("TC_MAX_TRIES", "120")),
-        sleep_time=float(os.getenv("TC_SLEEP_TIME", "1")),
-    )
-    print("Successfully set waiting utils config")
-  except ModuleNotFoundError:
+    _configure_testcontainers_timeouts()
+  except ImportError:
     print("The testcontainers library is not installed.")
 
   TestPipeline.pytest_test_pipeline_options = config.getoption(

Reply via email to