diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/__init__.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/__init__.py index c4cff9be0634..589ca0f7adb9 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/__init__.py @@ -118,8 +118,8 @@ from .types.assets import TimeWindow from .types.assets import VersionedResource -api_core.check_python_version("google.cloud.asset_v1") -api_core.check_dependency_versions("google.cloud.asset_v1") +api_core.check_python_version("") +api_core.check_dependency_versions("") __all__ = ( 'AssetServiceAsyncClient', diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/__init__.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/__init__.py index 9c0c65db39c0..f677d1815122 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/__init__.py @@ -44,8 +44,8 @@ from .types.common import SignJwtRequest from .types.common import SignJwtResponse -api_core.check_python_version("google.iam.credentials_v1") -api_core.check_dependency_versions("google.iam.credentials_v1") +api_core.check_python_version("") +api_core.check_dependency_versions("") __all__ = ( 'IAMCredentialsAsyncClient', diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/__init__.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/__init__.py index 5e2f12d53746..62609e4635cd 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/__init__.py @@ -120,8 +120,8 @@ from .types.logging_metrics import LogMetric from .types.logging_metrics import UpdateLogMetricRequest -api_core.check_python_version("google.cloud.logging_v2") -api_core.check_dependency_versions("google.cloud.logging_v2") +api_core.check_python_version("") +api_core.check_dependency_versions("") __all__ = ( 'ConfigServiceV2AsyncClient', diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/__init__.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/__init__.py index b58fac5882bb..e138052ae3de 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/__init__.py @@ -120,8 +120,8 @@ from .types.logging_metrics import LogMetric from .types.logging_metrics import UpdateLogMetricRequest -api_core.check_python_version("google.cloud.logging_v2") -api_core.check_dependency_versions("google.cloud.logging_v2") +api_core.check_python_version("") +api_core.check_dependency_versions("") __all__ = ( 'BaseConfigServiceV2AsyncClient', diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/__init__.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/__init__.py index 1b8d17cc75ca..7b84c8338fd3 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/__init__.py @@ -55,8 +55,8 @@ from .types.cloud_redis import WeeklyMaintenanceWindow from .types.cloud_redis import ZoneMetadata -api_core.check_python_version("google.cloud.redis_v1") -api_core.check_dependency_versions("google.cloud.redis_v1") +api_core.check_python_version("") +api_core.check_dependency_versions("") __all__ = ( 'CloudRedisAsyncClient', diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/__init__.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/__init__.py index 08bda73fa39f..7c5150513e9b 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/__init__.py @@ -64,8 +64,8 @@ from .types.storage_batch_operations_types import RewriteObject from .types.storage_batch_operations_types import UpdateObjectCustomContext -api_core.check_python_version("google.cloud.storagebatchoperations_v1") -api_core.check_dependency_versions("google.cloud.storagebatchoperations_v1") +api_core.check_python_version("") +api_core.check_dependency_versions("") __all__ = ( 'StorageBatchOperationsAsyncClient', diff --git a/packages/google-api-core/google/api_core/_python_package_support.py b/packages/google-api-core/google/api_core/_python_package_support.py index a6c35d8c1804..25aa4cd322c8 100644 --- a/packages/google-api-core/google/api_core/_python_package_support.py +++ b/packages/google-api-core/google/api_core/_python_package_support.py @@ -30,14 +30,37 @@ # about deprecated and unsupported versions. DependencyConstraint = namedtuple( "DependencyConstraint", - ["package_name", "minimum_fully_supported_version", "recommended_version"], + [ + "package_name", + "minimum_fully_supported_version", + "recommended_version", + "message_template", + ], + defaults=(None, None), ) + +PQC_GRPC_WARNING_TEMPLATE = ( + "Package {consumer_package} depends on {dependency_package}, currently installed at version {version_used_string}. " + "grpcio < 1.83.0 does not support Post-Quantum Cryptography (PQC). " + "Support for non-PQC environments is deprecated. In October 2026, " + "Google Cloud Python packages will raise their minimum requirements " + "(including google-api-core, grpcio, and grpcio-status) to enforce grpcio >= 1.83.0. " + "For more details on Google Cloud's post-quantum security migration, visit: " + "https://cloud.google.com/security/resources/post-quantum-cryptography" +) + _PACKAGE_DEPENDENCY_WARNINGS = [ DependencyConstraint( "google.protobuf", minimum_fully_supported_version="6.33.5", recommended_version="6.x", - ) + ), + DependencyConstraint( + "grpcio", + minimum_fully_supported_version="1.83.0", + recommended_version="1.83.x", + message_template=PQC_GRPC_WARNING_TEMPLATE, + ), ] @@ -50,8 +73,8 @@ def parse_version_to_tuple(version_string: str) -> ParsedVersion: """Safely converts a semantic version string to a comparable tuple of integers. - Example: "6.33.5" -> (6, 33, 5) - Ignores non-numeric parts and handles common version formats. + Example: "6.33.5" -> (6, 33, 5), "1.83.1rc1" -> (1, 83, 1) + Parses leading digits of each component to correctly handle pre-releases. Args: version_string: Version string in the format "x.y.z" or "x.y.z" @@ -61,12 +84,14 @@ def parse_version_to_tuple(version_string: str) -> ParsedVersion: """ parts = [] for part in version_string.split("."): - try: - parts.append(int(part)) - except ValueError: - # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here. - # This is a simplification compared to 'packaging.parse_version', but sufficient - # for comparing strictly numeric semantic versions. + digits = "" + for c in part: + if not c.isdigit(): + break + digits += c + if digits: + parts.append(int(digits)) + else: break return tuple(parts) @@ -162,7 +187,7 @@ def warn_deprecation_for_versions_less_than( ) = _get_distribution_and_import_packages(consumer_import_package) recommendation = ( - " (we recommend {recommended_version})" if recommended_version else "" + f" (we recommend {recommended_version})" if recommended_version else "" ) message_template = message_template or _flatten_message( """ @@ -222,4 +247,5 @@ def check_dependency_versions( package_info.package_name, package_info.minimum_fully_supported_version, recommended_version=package_info.recommended_version, + message_template=package_info.message_template, ) diff --git a/packages/google-api-core/tests/unit/test_python_package_support.py b/packages/google-api-core/tests/unit/test_python_package_support.py index 42e8de673dea..76ed6eed837b 100644 --- a/packages/google-api-core/tests/unit/test_python_package_support.py +++ b/packages/google-api-core/tests/unit/test_python_package_support.py @@ -18,6 +18,7 @@ import pytest from google.api_core._python_package_support import ( + PQC_GRPC_WARNING_TEMPLATE, DependencyConstraint, DependencyVersion, check_dependency_versions, @@ -37,14 +38,20 @@ def test_get_dependency_version(mocker, version_string_to_test): parse_version_to_tuple(version_string_to_test), version_string_to_test ) assert get_dependency_version("some-package") == expected - mock_importlib.assert_called_once_with("some-package") # Test package not found - mock_importlib.side_effect = ImportError + mock_importlib.side_effect = Exception assert get_dependency_version("not-a-package") == DependencyVersion(None, "--") +def test_parse_version_to_tuple_prerelease(): + """Test parse_version_to_tuple with pre-release versions.""" + assert parse_version_to_tuple("1.83.1rc1") == (1, 83, 1) + assert parse_version_to_tuple("1.83.0dev0") == (1, 83, 0) + assert parse_version_to_tuple("6.33.5b2") == (6, 33, 5) + + @patch("google.api_core._python_package_support._get_distribution_and_import_packages") @patch("google.api_core._python_package_support.get_dependency_version") def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_packages): @@ -125,8 +132,40 @@ def test_check_dependency_versions_with_custom_warnings(mock_warn): assert mock_warn.call_count == 2 mock_warn.assert_any_call( - "my-consumer", "pkg1", "1.0.0", recommended_version="2.0.0" + "my-consumer", + "pkg1", + "1.0.0", + recommended_version="2.0.0", + message_template=None, + ) + mock_warn.assert_any_call( + "my-consumer", + "pkg2", + "2.0.0", + recommended_version="3.0.0", + message_template=None, + ) + + +@patch( + "google.api_core._python_package_support.warn_deprecation_for_versions_less_than" +) +def test_check_dependency_versions_default(mock_warn): + """Test check_dependency_versions with default package dependency warnings.""" + check_dependency_versions("my-consumer") + + assert mock_warn.call_count == 2 + mock_warn.assert_any_call( + "my-consumer", + "google.protobuf", + "6.33.5", + recommended_version="6.x", + message_template=None, ) mock_warn.assert_any_call( - "my-consumer", "pkg2", "2.0.0", recommended_version="3.0.0" + "my-consumer", + "grpcio", + "1.83.0", + recommended_version="1.83.x", + message_template=PQC_GRPC_WARNING_TEMPLATE, ) diff --git a/packages/google-auth/google/auth/_helpers.py b/packages/google-auth/google/auth/_helpers.py index 86c48c1e525c..3abc0b6cc9fd 100644 --- a/packages/google-auth/google/auth/_helpers.py +++ b/packages/google-auth/google/auth/_helpers.py @@ -534,3 +534,19 @@ def response_log(logger: logging.Logger, response: Any) -> None: if is_logging_enabled(logger): json_response = _parse_response(response) _response_log_base(logger, json_response) + + +def _parse_version_to_tuple(version_string): + """Safely converts a semantic version string to a comparable tuple of integers.""" + parts = [] + for part in version_string.split("."): + digits = "" + for c in part: + if not c.isdigit(): + break + digits += c + if digits: + parts.append(int(digits)) + else: + break + return tuple(parts) diff --git a/packages/google-auth/google/auth/transport/grpc.py b/packages/google-auth/google/auth/transport/grpc.py index 5930a07f1d08..13e8077e724c 100644 --- a/packages/google-auth/google/auth/transport/grpc.py +++ b/packages/google-auth/google/auth/transport/grpc.py @@ -18,6 +18,7 @@ import logging +from google.auth import _helpers from google.auth import exceptions from google.auth.transport import _mtls_helper from google.auth.transport import mtls @@ -30,6 +31,25 @@ "gRPC is not installed from please install the grpcio package to use the gRPC transport." ) from caught_exc + +try: + import importlib.metadata + import warnings + + _grpc_ver_str = importlib.metadata.version("grpcio") + if _helpers._parse_version_to_tuple(_grpc_ver_str) < (1, 83, 0): + warnings.warn( + "grpcio < 1.83.0 does not support Post-Quantum Cryptography (PQC). " + "Support for non-PQC environments is deprecated. In October 2026, " + "google-auth will raise its minimum requirements " + "to enforce grpcio >= 1.83.0. " + "For more details on Google Cloud's post-quantum security migration, visit: " + "https://cloud.google.com/security/resources/post-quantum-cryptography", + FutureWarning, + ) +except Exception: + pass + _LOGGER = logging.getLogger(__name__) diff --git a/packages/google-auth/tests/test__helpers.py b/packages/google-auth/tests/test__helpers.py index e2d8e7b20c03..ad2de3eeca6e 100644 --- a/packages/google-auth/tests/test__helpers.py +++ b/packages/google-auth/tests/test__helpers.py @@ -697,3 +697,10 @@ def test_out_of_range(self): with pytest.raises((OverflowError, OSError, ValueError)): # Large enough to fail on most systems (Year 300,000+) _helpers.utcfromtimestamp(9999999999999) + + +def test_parse_version_to_tuple(): + assert _helpers._parse_version_to_tuple("1.83.0") == (1, 83, 0) + assert _helpers._parse_version_to_tuple("1.83.1rc1") == (1, 83, 1) + assert _helpers._parse_version_to_tuple("1.83.0dev0") == (1, 83, 0) + assert _helpers._parse_version_to_tuple("2.0") == (2, 0) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/__init__.py b/packages/google-cloud-bigquery/google/cloud/bigquery/__init__.py index fa03156e2e26..fd0a81c9bf9a 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/__init__.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/__init__.py @@ -34,6 +34,10 @@ __version__ = bigquery_version.__version__ +import google.api_core as api_core + +api_core.check_python_version("google.cloud.bigquery") +api_core.check_dependency_versions("google.cloud.bigquery") from google.cloud.bigquery.client import Client from google.cloud.bigquery.dataset import AccessEntry from google.cloud.bigquery.dataset import Dataset @@ -123,14 +127,6 @@ except ImportError: bigquery_magics = None -if sys.version_info < (3, 10): # pragma: NO COVER - warnings.warn( - "The python-bigquery library no longer supports Python <= 3.9. " - f"Your Python version is {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}. We " - "recommend that you update soon to ensure ongoing support. For " - "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)", - FutureWarning, - ) __all__ = [ "__version__", diff --git a/packages/google-cloud-bigquery/tests/unit/test__helpers.py b/packages/google-cloud-bigquery/tests/unit/test__helpers.py index 4e53236e39e7..861954fd6269 100644 --- a/packages/google-cloud-bigquery/tests/unit/test__helpers.py +++ b/packages/google-cloud-bigquery/tests/unit/test__helpers.py @@ -1214,3 +1214,4 @@ def test__valid_isinstance_or_raise(self, value, dtype, none_allowed, expected): def test__invalid_isinstance_or_raise(self, value, dtype, none_allowed, expected): with expected: _isinstance_or_raise(value, dtype, none_allowed=none_allowed) + diff --git a/packages/google-cloud-ndb/google/cloud/ndb/__init__.py b/packages/google-cloud-ndb/google/cloud/ndb/__init__.py index 153ff9d1db7c..291d2a6aa074 100644 --- a/packages/google-cloud-ndb/google/cloud/ndb/__init__.py +++ b/packages/google-cloud-ndb/google/cloud/ndb/__init__.py @@ -21,8 +21,15 @@ .. autodata:: __all__ """ +from google.cloud.ndb import utils from google.cloud.ndb import version + +import google.api_core as api_core + +api_core.check_python_version("google.cloud.ndb") +api_core.check_dependency_versions("google.cloud.ndb") + __version__: str = version.__version__ from google.cloud.ndb._datastore_api import EVENTUAL, EVENTUAL_CONSISTENCY, STRONG diff --git a/packages/google-cloud-ndb/google/cloud/ndb/utils.py b/packages/google-cloud-ndb/google/cloud/ndb/utils.py index aecf861cd52c..833c4c31c6e3 100644 --- a/packages/google-cloud-ndb/google/cloud/ndb/utils.py +++ b/packages/google-cloud-ndb/google/cloud/ndb/utils.py @@ -163,3 +163,4 @@ def tweak_logging(*args, **kwargs): def wrapping(*args, **kwargs): """Use functools.wraps instead""" raise NotImplementedError + diff --git a/packages/google-cloud-ndb/tests/unit/test_utils.py b/packages/google-cloud-ndb/tests/unit/test_utils.py index 571aa5ada0ed..3041414a90d6 100644 --- a/packages/google-cloud-ndb/tests/unit/test_utils.py +++ b/packages/google-cloud-ndb/tests/unit/test_utils.py @@ -133,3 +133,4 @@ def test_tweak_logging(): def test_wrapping(): with pytest.raises(NotImplementedError): utils.wrapping() +