From cee046b005c290fd909254a249af4e45d4629057 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 30 Jul 2026 20:56:23 +0000 Subject: [PATCH 01/11] feat(bigquery): add pandas-gbq capability helper --- .../cloud/bigquery/_versions_helpers.py | 43 ++++- .../tests/unit/test__pandas_helpers.py | 64 +++++-- .../tests/unit/test__pyarrow_helpers.py | 13 ++ .../tests/unit/test__versions_helpers.py | 164 +++++++++++++++--- 4 files changed, 244 insertions(+), 40 deletions(-) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py index d856c19852e7..a9253448bc22 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py @@ -19,7 +19,6 @@ from google.cloud.bigquery import exceptions - _MIN_PYARROW_VERSION = packaging.version.Version("3.0.0") _MIN_BQ_STORAGE_VERSION = packaging.version.Version("2.0.0") _BQ_STORAGE_OPTIONAL_READ_SESSION_VERSION = packaging.version.Version("2.6.0") @@ -247,3 +246,45 @@ def try_import(self, raise_if_error: bool = False) -> Any: and PYARROW_VERSIONS.try_import() is not None and PYARROW_VERSIONS.installed_version >= _MIN_PYARROW_VERSION_RANGE ) + + +class PandasGBQVersions: + """Version and delegation comparisons for pandas-gbq package.""" + + def __init__(self): + self._installed_version = None + self._delegation_api_version = None + + @property + def installed_version(self) -> packaging.version.Version: + """Return the parsed version of pandas-gbq""" + if self._installed_version is not None: + return self._installed_version + + try: + import pandas_gbq # type: ignore + + return packaging.version.parse(getattr(pandas_gbq, "__version__", "0.0.0")) + except Exception: + return packaging.version.parse("0.0.0") + + @property + def delegation_api_version(self) -> int: + """Return the delegation API version of pandas-gbq if installed, otherwise 0.""" + if self._delegation_api_version is not None: + return self._delegation_api_version + + try: + import pandas_gbq # type: ignore + + return int(getattr(pandas_gbq, "_internal_delegation_api_version", 0)) + except Exception: + return 0 + + @property + def is_delegation_supported(self) -> bool: + """True if the installed pandas-gbq version supports query delegation API (version >= 1).""" + return self.delegation_api_version >= 1 + + +PANDAS_GBQ_VERSIONS = PandasGBQVersions() diff --git a/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py b/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py index 34da6370e039..b22ad6850e44 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py @@ -18,6 +18,7 @@ import decimal import functools import gc +import importlib.metadata as metadata import operator import queue import time @@ -25,8 +26,6 @@ from unittest import mock import warnings -import importlib.metadata as metadata - try: import pandas import pandas.api.types @@ -47,11 +46,12 @@ import pytest from google import api_core - -from google.cloud.bigquery import exceptions -from google.cloud.bigquery import _pyarrow_helpers -from google.cloud.bigquery import _versions_helpers -from google.cloud.bigquery import schema +from google.cloud.bigquery import ( + _pyarrow_helpers, + _versions_helpers, + exceptions, + schema, +) from google.cloud.bigquery._pandas_helpers import determine_requested_streams pyarrow = _versions_helpers.PYARROW_VERSIONS.try_import() @@ -1831,8 +1831,7 @@ def test__download_table_bqstorage( expected_call_count, expected_maxsize, ): - from google.cloud.bigquery import dataset - from google.cloud.bigquery import table + from google.cloud.bigquery import dataset, table queue_used = None # A reference to the queue used by code under test. @@ -1885,11 +1884,11 @@ def test__download_table_bqstorage_shuts_down_workers( the child threads are also stopped. """ pytest.importorskip("google.cloud.bigquery_storage_v1") - from google.cloud.bigquery import dataset - from google.cloud.bigquery import table import google.cloud.bigquery_storage_v1.reader import google.cloud.bigquery_storage_v1.types + from google.cloud.bigquery import dataset, table + monkeypatch.setattr( _versions_helpers.BQ_STORAGE_VERSIONS, "_installed_version", None ) @@ -2211,10 +2210,10 @@ def test_determine_requested_streams_invalid_max_stream_count(): bigquery_storage is None, reason="Requires google-cloud-bigquery-storage" ) def test__download_table_bqstorage_w_timeout_error(module_under_test): - from google.cloud.bigquery import dataset - from google.cloud.bigquery import table from unittest import mock + from google.cloud.bigquery import dataset, table + mock_bqstorage_client = mock.create_autospec( bigquery_storage.BigQueryReadClient, instance=True ) @@ -2248,10 +2247,10 @@ def slow_download_stream( bigquery_storage is None, reason="Requires google-cloud-bigquery-storage" ) def test__download_table_bqstorage_w_timeout_success(module_under_test): - from google.cloud.bigquery import dataset - from google.cloud.bigquery import table from unittest import mock + from google.cloud.bigquery import dataset, table + mock_bqstorage_client = mock.create_autospec( bigquery_storage.BigQueryReadClient, instance=True ) @@ -2409,3 +2408,38 @@ def test_download_arrow_bqstorage_passes_timeout_to_create_read_session( assert retry_policy is not None # Check if deadline is set correctly in the retry policy assert retry_policy._deadline == timeout + + +@pytest.mark.skipif(pandas is None, reason="Requires `pandas`") +def test_dataframe_to_bq_schema_w_unused_schema_field(module_under_test): + with mock.patch.object(module_under_test, "pandas_gbq", None): + with pytest.raises( + ValueError, match="bq_schema contains fields not present in dataframe" + ): + module_under_test.dataframe_to_bq_schema( + pandas.DataFrame(), (schema.SchemaField("not_in_df", "STRING"),) + ) + + +@pytest.mark.skipif(pandas is None, reason="Requires `pandas`") +@pytest.mark.skipif(isinstance(pyarrow, mock.Mock), reason="Requires `pyarrow`") +def test_get_schema_by_pyarrow_bignumeric(module_under_test): + series = pandas.Series([decimal.Decimal("1.12345678901")]) + result = module_under_test._get_schema_by_pyarrow("col", series) + assert result is not None + assert result.field_type == "BIGNUMERIC" + + +@pytest.mark.skipif(pandas is None, reason="Requires `pandas`") +@pytest.mark.skipif(isinstance(pyarrow, mock.Mock), reason="Requires `pyarrow`") +def test_get_types_mapper_range_timestamp_mismatch(module_under_test): + if not hasattr(pandas, "ArrowDtype"): + return + range_ts = pandas.ArrowDtype( + pyarrow.struct( + [("start", pyarrow.timestamp("us")), ("end", pyarrow.timestamp("us"))] + ) + ) + mapper = module_under_test.default_types_mapper(range_timestamp_dtype=range_ts) + unmatched_struct = pyarrow.struct([("other", pyarrow.int64())]) + assert mapper(unmatched_struct) is None diff --git a/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py b/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py index c12a526de5d3..a4e9c0c9dc78 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py @@ -44,3 +44,16 @@ def test_bq_to_arrow_scalars(module_under_test): def test_arrow_scalar_ids_to_bq(module_under_test): assert module_under_test.arrow_scalar_ids_to_bq(pyarrow.bool_().id) == "BOOL" assert module_under_test.arrow_scalar_ids_to_bq("UNKNOWN_TYPE") is None + + +def test_pyarrow_helpers_when_pyarrow_none(module_under_test): + import importlib + import sys + from unittest import mock + + with mock.patch.dict(sys.modules, {"pyarrow": None}): + importlib.reload(module_under_test) + assert module_under_test.pyarrow is None + assert module_under_test.arrow_scalar_ids_to_bq(1) is None + + importlib.reload(module_under_test) diff --git a/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py b/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py index 8379c87c18e0..06ce47104cb5 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py @@ -31,8 +31,7 @@ except ImportError: pandas = None -from google.cloud.bigquery import _versions_helpers -from google.cloud.bigquery import exceptions +from google.cloud.bigquery import _versions_helpers, exceptions @pytest.mark.skipif(pyarrow is None, reason="pyarrow is not installed") @@ -59,14 +58,14 @@ def test_try_import_raises_error_w_legacy_pyarrow(): versions.try_import(raise_if_error=True) -@pytest.mark.skipif( - pyarrow is not None, - reason="pyarrow is installed, but this test needs it not to be", -) def test_try_import_raises_error_w_no_pyarrow(): + import sys + versions = _versions_helpers.PyarrowVersions() - with pytest.raises(exceptions.LegacyPyarrowError): - versions.try_import(raise_if_error=True) + with mock.patch.dict(sys.modules, {"pyarrow": None}): + assert versions.try_import(raise_if_error=False) is None + with pytest.raises(exceptions.LegacyPyarrowError): + versions.try_import(raise_if_error=True) @pytest.mark.skipif(pyarrow is None, reason="pyarrow is not installed") @@ -122,17 +121,29 @@ def test_returns_none_with_legacy_bqstorage(): assert bq_storage is None -@pytest.mark.skipif( - bigquery_storage is not None, - reason="Tests behavior when `google-cloud-bigquery-storage` isn't installed", -) def test_returns_none_with_bqstorage_uninstalled(): - try: - bqstorage_versions = _versions_helpers.BQStorageVersions() - bq_storage = bqstorage_versions.try_import() - except exceptions.LegacyBigQueryStorageError: # pragma: NO COVER - raise ("NotFound error raised when raise_if_error == False.") - assert bq_storage is None + import sys + + from google import cloud + + versions = _versions_helpers.BQStorageVersions() + with mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}): + with mock.patch.dict(cloud.__dict__): + cloud.__dict__.pop("bigquery_storage", None) + assert versions.try_import() is None + + +def test_raises_error_with_bqstorage_uninstalled(): + import sys + + from google import cloud + + versions = _versions_helpers.BQStorageVersions() + with mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}): + with mock.patch.dict(cloud.__dict__): + cloud.__dict__.pop("bigquery_storage", None) + with pytest.raises(exceptions.BigQueryStorageNotFoundError): + versions.try_import(raise_if_error=True) @pytest.mark.skipif( @@ -220,14 +231,14 @@ def test_try_import_raises_error_w_legacy_pandas(): versions.try_import(raise_if_error=True) -@pytest.mark.skipif( - pandas is not None, - reason="pandas is installed, but this test needs it not to be", -) def test_try_import_raises_error_w_no_pandas(): + import sys + versions = _versions_helpers.PandasVersions() - with pytest.raises(exceptions.LegacyPandasError): - versions.try_import(raise_if_error=True) + with mock.patch.dict(sys.modules, {"pandas": None}): + assert versions.try_import(raise_if_error=False) is None + with pytest.raises(exceptions.LegacyPandasError): + versions.try_import(raise_if_error=True) @pytest.mark.skipif(pandas is None, reason="pandas is not installed") @@ -246,3 +257,108 @@ def test_installed_pandas_version_returns_parsed_version(): assert version.major == 1 assert version.minor == 1 assert version.micro == 0 + + +def test_installed_pandas_gbq_version_returns_cached(): + versions = _versions_helpers.PandasGBQVersions() + versions._installed_version = object() + assert versions.installed_version is versions._installed_version + + +def test_installed_pandas_gbq_version_returns_parsed_version(): + import sys + + mock_pandas_gbq = mock.Mock() + mock_pandas_gbq.__version__ = "1.2.3" + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": mock_pandas_gbq}): + version = versions.installed_version + + assert version.major == 1 + assert version.minor == 2 + assert version.micro == 3 + + +def test_installed_pandas_gbq_version_falls_back_on_import_error(): + import sys + + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": None}): + version = versions.installed_version + + assert version.major == 0 + assert version.minor == 0 + assert version.micro == 0 + + +def test_installed_pandas_gbq_version_falls_back_on_other_error(): + import sys + + # Simulate a corrupted package raising an error on import/property access + class CorruptPandasGBQ: + @property + def __version__(self): + raise TypeError("Corrupted package") + + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": CorruptPandasGBQ()}): + version = versions.installed_version + + assert version.major == 0 + assert version.minor == 0 + assert version.micro == 0 + + +def test_pandas_gbq_delegation_api_version_returns_cached(): + versions = _versions_helpers.PandasGBQVersions() + versions._delegation_api_version = object() + assert versions.delegation_api_version is versions._delegation_api_version + + +def test_pandas_gbq_delegation_api_version_returns_value(): + import sys + + mock_pandas_gbq = mock.Mock() + mock_pandas_gbq._internal_delegation_api_version = 42 + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": mock_pandas_gbq}): + version = versions.delegation_api_version + + assert version == 42 + + +def test_pandas_gbq_delegation_api_version_falls_back_on_import_error(): + import sys + + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": None}): + version = versions.delegation_api_version + + assert version == 0 + + +def test_pandas_gbq_delegation_api_version_falls_back_on_other_error(): + import sys + + class CorruptPandasGBQ: + @property + def _internal_delegation_api_version(self): + raise TypeError("Corrupted package") + + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": CorruptPandasGBQ()}): + version = versions.delegation_api_version + + assert version == 0 + + +def test_pandas_gbq_is_delegation_supported_true(): + versions = _versions_helpers.PandasGBQVersions() + versions._delegation_api_version = 1 + assert versions.is_delegation_supported is True + + +def test_pandas_gbq_is_delegation_supported_false(): + versions = _versions_helpers.PandasGBQVersions() + versions._delegation_api_version = 0 + assert versions.is_delegation_supported is False From ce60fcf201a1e1c13d973a3e600504ced8a504ab Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 30 Jul 2026 14:08:17 -0700 Subject: [PATCH 02/11] Update packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../google/cloud/bigquery/_versions_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py index a9253448bc22..b2cb300b86f8 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py @@ -277,9 +277,10 @@ def delegation_api_version(self) -> int: try: import pandas_gbq # type: ignore - return int(getattr(pandas_gbq, "_internal_delegation_api_version", 0)) + self._delegation_api_version = int(getattr(pandas_gbq, "_internal_delegation_api_version", 0)) except Exception: - return 0 + self._delegation_api_version = 0 + return self._delegation_api_version @property def is_delegation_supported(self) -> bool: From eb6a1d469d039b74efc5ac3cfdce0b9a15c26a8c Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 30 Jul 2026 14:08:26 -0700 Subject: [PATCH 03/11] Update packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../google/cloud/bigquery/_versions_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py index b2cb300b86f8..3245c3f9d42b 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py @@ -264,9 +264,10 @@ def installed_version(self) -> packaging.version.Version: try: import pandas_gbq # type: ignore - return packaging.version.parse(getattr(pandas_gbq, "__version__", "0.0.0")) + self._installed_version = packaging.version.parse(getattr(pandas_gbq, "__version__", "0.0.0")) except Exception: - return packaging.version.parse("0.0.0") + self._installed_version = packaging.version.parse("0.0.0") + return self._installed_version @property def delegation_api_version(self) -> int: From 47a04f066b6a73d036c05e1519f88e4bc44b7f4b Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 30 Jul 2026 21:15:22 +0000 Subject: [PATCH 04/11] style: fix line length in _versions_helpers.py --- .../google/cloud/bigquery/_versions_helpers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py index 3245c3f9d42b..4edc5f5ba3cb 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py @@ -264,7 +264,9 @@ def installed_version(self) -> packaging.version.Version: try: import pandas_gbq # type: ignore - self._installed_version = packaging.version.parse(getattr(pandas_gbq, "__version__", "0.0.0")) + self._installed_version = packaging.version.parse( + getattr(pandas_gbq, "__version__", "0.0.0") + ) except Exception: self._installed_version = packaging.version.parse("0.0.0") return self._installed_version @@ -278,7 +280,9 @@ def delegation_api_version(self) -> int: try: import pandas_gbq # type: ignore - self._delegation_api_version = int(getattr(pandas_gbq, "_internal_delegation_api_version", 0)) + self._delegation_api_version = int( + getattr(pandas_gbq, "_internal_delegation_api_version", 0) + ) except Exception: self._delegation_api_version = 0 return self._delegation_api_version From fd6bf9ea6e7935d992910e68fd49879b0ae96730 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 31 Jul 2026 18:24:40 +0000 Subject: [PATCH 05/11] fix(bigquery): harden test fixtures and credentials isolation --- .../tests/unit/test_magics.py | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/google-cloud-bigquery/tests/unit/test_magics.py b/packages/google-cloud-bigquery/tests/unit/test_magics.py index 6076363d8a93..542b7090541e 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_magics.py +++ b/packages/google-cloud-bigquery/tests/unit/test_magics.py @@ -165,6 +165,16 @@ def test_context_resolves_unset_credentials_and_project(): assert default_mock.call_count == 2 +def test_context_fallback_when_bigquery_magics_none(): + ctx = magics.Context() + credentials_mock = mock.create_autospec( + google.auth.credentials.Credentials, instance=True + ) + with mock.patch("google.auth.default", return_value=(credentials_mock, "proj-123")): + assert ctx.credentials is credentials_mock + assert ctx.project == "proj-123" + + @pytest.mark.usefixtures("ipython_interactive") @pytest.mark.skipif(pandas is None, reason="Requires `pandas`") def test_context_with_default_connection(monkeypatch): @@ -675,9 +685,11 @@ def test_bigquery_magic_with_bqstorage_from_argument( google.cloud.bigquery.job.QueryJob, instance=True ) query_job_mock.to_dataframe.return_value = result - with run_query_patch as run_query_mock, ( - bqstorage_client_patch - ), warnings.catch_warnings(record=True) as warned: + with ( + run_query_patch as run_query_mock, + bqstorage_client_patch, + warnings.catch_warnings(record=True) as warned, + ): run_query_mock.return_value = query_job_mock return_value = ip.run_cell_magic("bigquery", "--use_bqstorage_api", sql) @@ -843,11 +855,12 @@ def test_bigquery_magic_w_max_results_query_job_results_fails(monkeypatch): ) query_job_mock.result.side_effect = [[], OSError] - with pytest.raises( - OSError - ), client_query_patch as client_query_mock, ( - default_patch - ), close_transports_patch as close_transports: + with ( + pytest.raises(OSError), + client_query_patch as client_query_mock, + default_patch, + close_transports_patch as close_transports, + ): client_query_mock.return_value = query_job_mock ip.run_cell_magic("bigquery", "--max_results=5", sql) @@ -1966,9 +1979,10 @@ def test_bigquery_magic_nonexisting_query_variable(monkeypatch): ip.user_ns.pop("custom_query", None) # Make sure the variable does NOT exist. cell_body = "$custom_query" # Referring to a non-existing variable name. - with pytest.raises( - NameError, match=r".*custom_query does not exist.*" - ), run_query_patch as run_query_mock: + with ( + pytest.raises(NameError, match=r".*custom_query does not exist.*"), + run_query_patch as run_query_mock, + ): ip.run_cell_magic("bigquery", "", cell_body) run_query_mock.assert_not_called() @@ -1989,9 +2003,10 @@ def test_bigquery_magic_empty_query_variable_name(monkeypatch): ) cell_body = "$" # Not referring to any variable (name omitted). - with pytest.raises( - NameError, match=r"(?i).*missing query variable name.*" - ), run_query_patch as run_query_mock: + with ( + pytest.raises(NameError, match=r"(?i).*missing query variable name.*"), + run_query_patch as run_query_mock, + ): ip.run_cell_magic("bigquery", "", cell_body) run_query_mock.assert_not_called() @@ -2017,9 +2032,10 @@ def test_bigquery_magic_query_variable_non_string(ipython_ns_cleanup, monkeypatc ip.user_ns["custom_query"] = object() cell_body = "$custom_query" # Referring to a non-string variable. - with pytest.raises( - TypeError, match=r".*must be a string or a bytes-like.*" - ), run_query_patch as run_query_mock: + with ( + pytest.raises(TypeError, match=r".*must be a string or a bytes-like.*"), + run_query_patch as run_query_mock, + ): ip.run_cell_magic("bigquery", "", cell_body) run_query_mock.assert_not_called() @@ -2184,9 +2200,11 @@ def test_bigquery_magic_create_dataset_fails(monkeypatch): autospec=True, ) - with pytest.raises( - OSError - ), create_dataset_if_necessary_patch, close_transports_patch as close_transports: + with ( + pytest.raises(OSError), + create_dataset_if_necessary_patch, + close_transports_patch as close_transports, + ): ip.run_cell_magic( "bigquery", "--destination_table dataset_id.table_id", From 2f2489d5ac38f3fe830886a23aa82f03e6c2b861 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 31 Jul 2026 12:03:20 -0700 Subject: [PATCH 06/11] Update packages/google-cloud-bigquery/tests/unit/test_magics.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/google-cloud-bigquery/tests/unit/test_magics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-cloud-bigquery/tests/unit/test_magics.py b/packages/google-cloud-bigquery/tests/unit/test_magics.py index 542b7090541e..2165f899bd9c 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_magics.py +++ b/packages/google-cloud-bigquery/tests/unit/test_magics.py @@ -165,7 +165,7 @@ def test_context_resolves_unset_credentials_and_project(): assert default_mock.call_count == 2 -def test_context_fallback_when_bigquery_magics_none(): +def test_context_fallback_to_default_credentials(): ctx = magics.Context() credentials_mock = mock.create_autospec( google.auth.credentials.Credentials, instance=True From 5555e99e73fab1dc26c9cdecb04d3439d626650a Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 31 Jul 2026 12:03:28 -0700 Subject: [PATCH 07/11] Update packages/google-cloud-bigquery/tests/unit/test_magics.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/google-cloud-bigquery/tests/unit/test_magics.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/google-cloud-bigquery/tests/unit/test_magics.py b/packages/google-cloud-bigquery/tests/unit/test_magics.py index 2165f899bd9c..6d79ffb387b7 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_magics.py +++ b/packages/google-cloud-bigquery/tests/unit/test_magics.py @@ -146,8 +146,6 @@ def test_context_resolves_unset_credentials_and_project(): """When context credentials and project are unset (None), accessing them resolves them from Application Default Credentials. """ - magics.context._credentials = None - magics.context._project = None assert magics.context._credentials is None assert magics.context._project is None From d4ab2e883c86af1665294d1a2e19582a4b78922f Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 31 Jul 2026 19:06:40 +0000 Subject: [PATCH 08/11] test(bigquery): fix Python 3.8 context manager syntax in test_magics.py --- .../tests/unit/test_magics.py | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/google-cloud-bigquery/tests/unit/test_magics.py b/packages/google-cloud-bigquery/tests/unit/test_magics.py index 6d79ffb387b7..94c97c89bef3 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_magics.py +++ b/packages/google-cloud-bigquery/tests/unit/test_magics.py @@ -146,6 +146,8 @@ def test_context_resolves_unset_credentials_and_project(): """When context credentials and project are unset (None), accessing them resolves them from Application Default Credentials. """ + magics.context._credentials = None + magics.context._project = None assert magics.context._credentials is None assert magics.context._project is None @@ -683,11 +685,9 @@ def test_bigquery_magic_with_bqstorage_from_argument( google.cloud.bigquery.job.QueryJob, instance=True ) query_job_mock.to_dataframe.return_value = result - with ( - run_query_patch as run_query_mock, - bqstorage_client_patch, - warnings.catch_warnings(record=True) as warned, - ): + with run_query_patch as run_query_mock, ( + bqstorage_client_patch + ), warnings.catch_warnings(record=True) as warned: run_query_mock.return_value = query_job_mock return_value = ip.run_cell_magic("bigquery", "--use_bqstorage_api", sql) @@ -853,12 +853,11 @@ def test_bigquery_magic_w_max_results_query_job_results_fails(monkeypatch): ) query_job_mock.result.side_effect = [[], OSError] - with ( - pytest.raises(OSError), - client_query_patch as client_query_mock, - default_patch, - close_transports_patch as close_transports, - ): + with pytest.raises( + OSError + ), client_query_patch as client_query_mock, ( + default_patch + ), close_transports_patch as close_transports: client_query_mock.return_value = query_job_mock ip.run_cell_magic("bigquery", "--max_results=5", sql) @@ -1977,10 +1976,9 @@ def test_bigquery_magic_nonexisting_query_variable(monkeypatch): ip.user_ns.pop("custom_query", None) # Make sure the variable does NOT exist. cell_body = "$custom_query" # Referring to a non-existing variable name. - with ( - pytest.raises(NameError, match=r".*custom_query does not exist.*"), - run_query_patch as run_query_mock, - ): + with pytest.raises( + NameError, match=r".*custom_query does not exist.*" + ), run_query_patch as run_query_mock: ip.run_cell_magic("bigquery", "", cell_body) run_query_mock.assert_not_called() @@ -2001,10 +1999,9 @@ def test_bigquery_magic_empty_query_variable_name(monkeypatch): ) cell_body = "$" # Not referring to any variable (name omitted). - with ( - pytest.raises(NameError, match=r"(?i).*missing query variable name.*"), - run_query_patch as run_query_mock, - ): + with pytest.raises( + NameError, match=r"(?i).*missing query variable name.*" + ), run_query_patch as run_query_mock: ip.run_cell_magic("bigquery", "", cell_body) run_query_mock.assert_not_called() @@ -2030,10 +2027,9 @@ def test_bigquery_magic_query_variable_non_string(ipython_ns_cleanup, monkeypatc ip.user_ns["custom_query"] = object() cell_body = "$custom_query" # Referring to a non-string variable. - with ( - pytest.raises(TypeError, match=r".*must be a string or a bytes-like.*"), - run_query_patch as run_query_mock, - ): + with pytest.raises( + TypeError, match=r".*must be a string or a bytes-like.*" + ), run_query_patch as run_query_mock: ip.run_cell_magic("bigquery", "", cell_body) run_query_mock.assert_not_called() @@ -2198,11 +2194,9 @@ def test_bigquery_magic_create_dataset_fails(monkeypatch): autospec=True, ) - with ( - pytest.raises(OSError), - create_dataset_if_necessary_patch, - close_transports_patch as close_transports, - ): + with pytest.raises( + OSError + ), create_dataset_if_necessary_patch, close_transports_patch as close_transports: ip.run_cell_magic( "bigquery", "--destination_table dataset_id.table_id", From e6dd8678971e1f19ad74571ff1424166a6545962 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Mon, 3 Aug 2026 23:17:05 +0000 Subject: [PATCH 09/11] feat(bigquery): align version helper with style guide --- .../cloud/bigquery/_versions_helpers.py | 3 +-- .../tests/unit/test__versions_helpers.py | 27 ++----------------- .../tests/unit/test_magics.py | 10 ------- 3 files changed, 3 insertions(+), 37 deletions(-) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py index 4edc5f5ba3cb..ecc4e22e9a0d 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py @@ -16,7 +16,6 @@ from typing import Any import packaging.version - from google.cloud.bigquery import exceptions _MIN_PYARROW_VERSION = packaging.version.Version("3.0.0") @@ -257,7 +256,7 @@ def __init__(self): @property def installed_version(self) -> packaging.version.Version: - """Return the parsed version of pandas-gbq""" + """Return the parsed version of pandas-gbq.""" if self._installed_version is not None: return self._installed_version diff --git a/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py b/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py index 06ce47104cb5..16a0ec50baea 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys from unittest import mock import pytest @@ -31,6 +32,7 @@ except ImportError: pandas = None +from google import cloud from google.cloud.bigquery import _versions_helpers, exceptions @@ -59,8 +61,6 @@ def test_try_import_raises_error_w_legacy_pyarrow(): def test_try_import_raises_error_w_no_pyarrow(): - import sys - versions = _versions_helpers.PyarrowVersions() with mock.patch.dict(sys.modules, {"pyarrow": None}): assert versions.try_import(raise_if_error=False) is None @@ -122,10 +122,6 @@ def test_returns_none_with_legacy_bqstorage(): def test_returns_none_with_bqstorage_uninstalled(): - import sys - - from google import cloud - versions = _versions_helpers.BQStorageVersions() with mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}): with mock.patch.dict(cloud.__dict__): @@ -134,10 +130,6 @@ def test_returns_none_with_bqstorage_uninstalled(): def test_raises_error_with_bqstorage_uninstalled(): - import sys - - from google import cloud - versions = _versions_helpers.BQStorageVersions() with mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}): with mock.patch.dict(cloud.__dict__): @@ -232,8 +224,6 @@ def test_try_import_raises_error_w_legacy_pandas(): def test_try_import_raises_error_w_no_pandas(): - import sys - versions = _versions_helpers.PandasVersions() with mock.patch.dict(sys.modules, {"pandas": None}): assert versions.try_import(raise_if_error=False) is None @@ -266,8 +256,6 @@ def test_installed_pandas_gbq_version_returns_cached(): def test_installed_pandas_gbq_version_returns_parsed_version(): - import sys - mock_pandas_gbq = mock.Mock() mock_pandas_gbq.__version__ = "1.2.3" versions = _versions_helpers.PandasGBQVersions() @@ -280,8 +268,6 @@ def test_installed_pandas_gbq_version_returns_parsed_version(): def test_installed_pandas_gbq_version_falls_back_on_import_error(): - import sys - versions = _versions_helpers.PandasGBQVersions() with mock.patch.dict(sys.modules, {"pandas_gbq": None}): version = versions.installed_version @@ -292,9 +278,6 @@ def test_installed_pandas_gbq_version_falls_back_on_import_error(): def test_installed_pandas_gbq_version_falls_back_on_other_error(): - import sys - - # Simulate a corrupted package raising an error on import/property access class CorruptPandasGBQ: @property def __version__(self): @@ -316,8 +299,6 @@ def test_pandas_gbq_delegation_api_version_returns_cached(): def test_pandas_gbq_delegation_api_version_returns_value(): - import sys - mock_pandas_gbq = mock.Mock() mock_pandas_gbq._internal_delegation_api_version = 42 versions = _versions_helpers.PandasGBQVersions() @@ -328,8 +309,6 @@ def test_pandas_gbq_delegation_api_version_returns_value(): def test_pandas_gbq_delegation_api_version_falls_back_on_import_error(): - import sys - versions = _versions_helpers.PandasGBQVersions() with mock.patch.dict(sys.modules, {"pandas_gbq": None}): version = versions.delegation_api_version @@ -338,8 +317,6 @@ def test_pandas_gbq_delegation_api_version_falls_back_on_import_error(): def test_pandas_gbq_delegation_api_version_falls_back_on_other_error(): - import sys - class CorruptPandasGBQ: @property def _internal_delegation_api_version(self): diff --git a/packages/google-cloud-bigquery/tests/unit/test_magics.py b/packages/google-cloud-bigquery/tests/unit/test_magics.py index 94c97c89bef3..6076363d8a93 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_magics.py +++ b/packages/google-cloud-bigquery/tests/unit/test_magics.py @@ -165,16 +165,6 @@ def test_context_resolves_unset_credentials_and_project(): assert default_mock.call_count == 2 -def test_context_fallback_to_default_credentials(): - ctx = magics.Context() - credentials_mock = mock.create_autospec( - google.auth.credentials.Credentials, instance=True - ) - with mock.patch("google.auth.default", return_value=(credentials_mock, "proj-123")): - assert ctx.credentials is credentials_mock - assert ctx.project == "proj-123" - - @pytest.mark.usefixtures("ipython_interactive") @pytest.mark.skipif(pandas is None, reason="Requires `pandas`") def test_context_with_default_connection(monkeypatch): From 8d6425a67df8e86e7c4084ee409a58dafcff61bd Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Mon, 3 Aug 2026 23:17:09 +0000 Subject: [PATCH 10/11] chore: remove unrelated test helper changes from PR 2 --- .../tests/unit/test__pandas_helpers.py | 64 +++++-------------- .../tests/unit/test__pyarrow_helpers.py | 13 ---- 2 files changed, 15 insertions(+), 62 deletions(-) diff --git a/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py b/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py index b22ad6850e44..34da6370e039 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__pandas_helpers.py @@ -18,7 +18,6 @@ import decimal import functools import gc -import importlib.metadata as metadata import operator import queue import time @@ -26,6 +25,8 @@ from unittest import mock import warnings +import importlib.metadata as metadata + try: import pandas import pandas.api.types @@ -46,12 +47,11 @@ import pytest from google import api_core -from google.cloud.bigquery import ( - _pyarrow_helpers, - _versions_helpers, - exceptions, - schema, -) + +from google.cloud.bigquery import exceptions +from google.cloud.bigquery import _pyarrow_helpers +from google.cloud.bigquery import _versions_helpers +from google.cloud.bigquery import schema from google.cloud.bigquery._pandas_helpers import determine_requested_streams pyarrow = _versions_helpers.PYARROW_VERSIONS.try_import() @@ -1831,7 +1831,8 @@ def test__download_table_bqstorage( expected_call_count, expected_maxsize, ): - from google.cloud.bigquery import dataset, table + from google.cloud.bigquery import dataset + from google.cloud.bigquery import table queue_used = None # A reference to the queue used by code under test. @@ -1884,11 +1885,11 @@ def test__download_table_bqstorage_shuts_down_workers( the child threads are also stopped. """ pytest.importorskip("google.cloud.bigquery_storage_v1") + from google.cloud.bigquery import dataset + from google.cloud.bigquery import table import google.cloud.bigquery_storage_v1.reader import google.cloud.bigquery_storage_v1.types - from google.cloud.bigquery import dataset, table - monkeypatch.setattr( _versions_helpers.BQ_STORAGE_VERSIONS, "_installed_version", None ) @@ -2210,10 +2211,10 @@ def test_determine_requested_streams_invalid_max_stream_count(): bigquery_storage is None, reason="Requires google-cloud-bigquery-storage" ) def test__download_table_bqstorage_w_timeout_error(module_under_test): + from google.cloud.bigquery import dataset + from google.cloud.bigquery import table from unittest import mock - from google.cloud.bigquery import dataset, table - mock_bqstorage_client = mock.create_autospec( bigquery_storage.BigQueryReadClient, instance=True ) @@ -2247,10 +2248,10 @@ def slow_download_stream( bigquery_storage is None, reason="Requires google-cloud-bigquery-storage" ) def test__download_table_bqstorage_w_timeout_success(module_under_test): + from google.cloud.bigquery import dataset + from google.cloud.bigquery import table from unittest import mock - from google.cloud.bigquery import dataset, table - mock_bqstorage_client = mock.create_autospec( bigquery_storage.BigQueryReadClient, instance=True ) @@ -2408,38 +2409,3 @@ def test_download_arrow_bqstorage_passes_timeout_to_create_read_session( assert retry_policy is not None # Check if deadline is set correctly in the retry policy assert retry_policy._deadline == timeout - - -@pytest.mark.skipif(pandas is None, reason="Requires `pandas`") -def test_dataframe_to_bq_schema_w_unused_schema_field(module_under_test): - with mock.patch.object(module_under_test, "pandas_gbq", None): - with pytest.raises( - ValueError, match="bq_schema contains fields not present in dataframe" - ): - module_under_test.dataframe_to_bq_schema( - pandas.DataFrame(), (schema.SchemaField("not_in_df", "STRING"),) - ) - - -@pytest.mark.skipif(pandas is None, reason="Requires `pandas`") -@pytest.mark.skipif(isinstance(pyarrow, mock.Mock), reason="Requires `pyarrow`") -def test_get_schema_by_pyarrow_bignumeric(module_under_test): - series = pandas.Series([decimal.Decimal("1.12345678901")]) - result = module_under_test._get_schema_by_pyarrow("col", series) - assert result is not None - assert result.field_type == "BIGNUMERIC" - - -@pytest.mark.skipif(pandas is None, reason="Requires `pandas`") -@pytest.mark.skipif(isinstance(pyarrow, mock.Mock), reason="Requires `pyarrow`") -def test_get_types_mapper_range_timestamp_mismatch(module_under_test): - if not hasattr(pandas, "ArrowDtype"): - return - range_ts = pandas.ArrowDtype( - pyarrow.struct( - [("start", pyarrow.timestamp("us")), ("end", pyarrow.timestamp("us"))] - ) - ) - mapper = module_under_test.default_types_mapper(range_timestamp_dtype=range_ts) - unmatched_struct = pyarrow.struct([("other", pyarrow.int64())]) - assert mapper(unmatched_struct) is None diff --git a/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py b/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py index a4e9c0c9dc78..c12a526de5d3 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__pyarrow_helpers.py @@ -44,16 +44,3 @@ def test_bq_to_arrow_scalars(module_under_test): def test_arrow_scalar_ids_to_bq(module_under_test): assert module_under_test.arrow_scalar_ids_to_bq(pyarrow.bool_().id) == "BOOL" assert module_under_test.arrow_scalar_ids_to_bq("UNKNOWN_TYPE") is None - - -def test_pyarrow_helpers_when_pyarrow_none(module_under_test): - import importlib - import sys - from unittest import mock - - with mock.patch.dict(sys.modules, {"pyarrow": None}): - importlib.reload(module_under_test) - assert module_under_test.pyarrow is None - assert module_under_test.arrow_scalar_ids_to_bq(1) is None - - importlib.reload(module_under_test) From 778d0e4f22231ec93761fa3bdb35eb27950c28fe Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 5 Aug 2026 00:39:03 +0000 Subject: [PATCH 11/11] fix(bigquery): address review feedback on pandas-gbq version helper --- .../cloud/bigquery/_versions_helpers.py | 16 ++-- .../tests/unit/test__versions_helpers.py | 92 ++++++++++++++++--- 2 files changed, 86 insertions(+), 22 deletions(-) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py index ecc4e22e9a0d..d9afc9781532 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/_versions_helpers.py @@ -25,6 +25,7 @@ _MIN_PANDAS_VERSION_RANGE = packaging.version.Version("1.5.0") _MIN_PYARROW_VERSION_RANGE = packaging.version.Version("10.0.1") +_MIN_PANDAS_GBQ_DELEGATION_VERSION = packaging.version.Version("1.0.0") class PyarrowVersions: @@ -271,25 +272,26 @@ def installed_version(self) -> packaging.version.Version: return self._installed_version @property - def delegation_api_version(self) -> int: - """Return the delegation API version of pandas-gbq if installed, otherwise 0.""" + def delegation_api_version(self) -> packaging.version.Version: + """Return the delegation API version of pandas-gbq if installed, otherwise 0.0.0.""" if self._delegation_api_version is not None: return self._delegation_api_version try: import pandas_gbq # type: ignore - self._delegation_api_version = int( - getattr(pandas_gbq, "_internal_delegation_api_version", 0) + raw_version = getattr( + pandas_gbq, "_internal_delegation_api_version", "0.0.0" ) + self._delegation_api_version = packaging.version.parse(str(raw_version)) except Exception: - self._delegation_api_version = 0 + self._delegation_api_version = packaging.version.parse("0.0.0") return self._delegation_api_version @property def is_delegation_supported(self) -> bool: - """True if the installed pandas-gbq version supports query delegation API (version >= 1).""" - return self.delegation_api_version >= 1 + """True if the installed pandas-gbq version supports query delegation API.""" + return self.delegation_api_version >= _MIN_PANDAS_GBQ_DELEGATION_VERSION PANDAS_GBQ_VERSIONS = PandasGBQVersions() diff --git a/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py b/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py index 16a0ec50baea..aec553bb7b19 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__versions_helpers.py @@ -13,8 +13,10 @@ # limitations under the License. import sys +import types from unittest import mock +import packaging.version import pytest try: @@ -72,6 +74,7 @@ def test_try_import_raises_error_w_no_pyarrow(): def test_installed_pyarrow_version_returns_cached(): versions = _versions_helpers.PyarrowVersions() versions._installed_version = object() + assert versions.installed_version is versions._installed_version @@ -123,19 +126,25 @@ def test_returns_none_with_legacy_bqstorage(): def test_returns_none_with_bqstorage_uninstalled(): versions = _versions_helpers.BQStorageVersions() - with mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}): - with mock.patch.dict(cloud.__dict__): - cloud.__dict__.pop("bigquery_storage", None) - assert versions.try_import() is None + with ( + mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}), + mock.patch.dict(cloud.__dict__), + ): + cloud.__dict__.pop("bigquery_storage", None) + + assert versions.try_import() is None def test_raises_error_with_bqstorage_uninstalled(): versions = _versions_helpers.BQStorageVersions() - with mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}): - with mock.patch.dict(cloud.__dict__): - cloud.__dict__.pop("bigquery_storage", None) - with pytest.raises(exceptions.BigQueryStorageNotFoundError): - versions.try_import(raise_if_error=True) + with ( + mock.patch.dict(sys.modules, {"google.cloud.bigquery_storage": None}), + mock.patch.dict(cloud.__dict__), + ): + cloud.__dict__.pop("bigquery_storage", None) + + with pytest.raises(exceptions.BigQueryStorageNotFoundError): + versions.try_import(raise_if_error=True) @pytest.mark.skipif( @@ -156,6 +165,7 @@ def test_raises_error_w_unknown_bqstorage_version(): def test_installed_bqstorage_version_returns_cached(): bqstorage_versions = _versions_helpers.BQStorageVersions() bqstorage_versions._installed_version = object() + assert bqstorage_versions.installed_version is bqstorage_versions._installed_version @@ -235,6 +245,7 @@ def test_try_import_raises_error_w_no_pandas(): def test_installed_pandas_version_returns_cached(): versions = _versions_helpers.PandasVersions() versions._installed_version = object() + assert versions.installed_version is versions._installed_version @@ -252,6 +263,7 @@ def test_installed_pandas_version_returns_parsed_version(): def test_installed_pandas_gbq_version_returns_cached(): versions = _versions_helpers.PandasGBQVersions() versions._installed_version = object() + assert versions.installed_version is versions._installed_version @@ -277,6 +289,17 @@ def test_installed_pandas_gbq_version_falls_back_on_import_error(): assert version.micro == 0 +def test_installed_pandas_gbq_version_falls_back_on_missing_version_attribute(): + mock_pandas_gbq = types.ModuleType("pandas_gbq") + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": mock_pandas_gbq}): + version = versions.installed_version + + assert version.major == 0 + assert version.minor == 0 + assert version.micro == 0 + + def test_installed_pandas_gbq_version_falls_back_on_other_error(): class CorruptPandasGBQ: @property @@ -295,17 +318,43 @@ def __version__(self): def test_pandas_gbq_delegation_api_version_returns_cached(): versions = _versions_helpers.PandasGBQVersions() versions._delegation_api_version = object() + assert versions.delegation_api_version is versions._delegation_api_version def test_pandas_gbq_delegation_api_version_returns_value(): mock_pandas_gbq = mock.Mock() - mock_pandas_gbq._internal_delegation_api_version = 42 + mock_pandas_gbq._internal_delegation_api_version = "1.2.3" + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": mock_pandas_gbq}): + version = versions.delegation_api_version + + assert version.major == 1 + assert version.minor == 2 + assert version.micro == 3 + + +def test_pandas_gbq_delegation_api_version_supports_integer_value(): + mock_pandas_gbq = mock.Mock() + mock_pandas_gbq._internal_delegation_api_version = 1 versions = _versions_helpers.PandasGBQVersions() with mock.patch.dict(sys.modules, {"pandas_gbq": mock_pandas_gbq}): version = versions.delegation_api_version - assert version == 42 + assert version.major == 1 + assert version.minor == 0 + assert version.micro == 0 + + +def test_pandas_gbq_delegation_api_version_falls_back_on_missing_attribute(): + mock_pandas_gbq = types.ModuleType("pandas_gbq") + versions = _versions_helpers.PandasGBQVersions() + with mock.patch.dict(sys.modules, {"pandas_gbq": mock_pandas_gbq}): + version = versions.delegation_api_version + + assert version.major == 0 + assert version.minor == 0 + assert version.micro == 0 def test_pandas_gbq_delegation_api_version_falls_back_on_import_error(): @@ -313,7 +362,9 @@ def test_pandas_gbq_delegation_api_version_falls_back_on_import_error(): with mock.patch.dict(sys.modules, {"pandas_gbq": None}): version = versions.delegation_api_version - assert version == 0 + assert version.major == 0 + assert version.minor == 0 + assert version.micro == 0 def test_pandas_gbq_delegation_api_version_falls_back_on_other_error(): @@ -326,16 +377,27 @@ def _internal_delegation_api_version(self): with mock.patch.dict(sys.modules, {"pandas_gbq": CorruptPandasGBQ()}): version = versions.delegation_api_version - assert version == 0 + assert version.major == 0 + assert version.minor == 0 + assert version.micro == 0 def test_pandas_gbq_is_delegation_supported_true(): versions = _versions_helpers.PandasGBQVersions() - versions._delegation_api_version = 1 + versions._delegation_api_version = packaging.version.Version("1.0.0") + + assert versions.is_delegation_supported is True + + +def test_pandas_gbq_is_delegation_supported_with_sub_minor_version(): + versions = _versions_helpers.PandasGBQVersions() + versions._delegation_api_version = packaging.version.Version("1.2.0") + assert versions.is_delegation_supported is True def test_pandas_gbq_is_delegation_supported_false(): versions = _versions_helpers.PandasGBQVersions() - versions._delegation_api_version = 0 + versions._delegation_api_version = packaging.version.Version("0.9.9") + assert versions.is_delegation_supported is False