Skip to content

Commit 2408166

Browse files
authored
feat(spanner): drop Python 3.7-3.9 support and regenerate (#17169)
Updates post processing to account for dropping support for Python 3.7, 3.8, 3.9 and the impacts that has on using 3.10 for lower bounds testing. ### Changes * updates the lower bound versions for several libraries to avoid conflicts and install issues in both `setup.py` and `constraints-3.10.txt` * updates post-processing scripts to ensure the above updates persist
1 parent 819ce1b commit 2408166

33 files changed

Lines changed: 392 additions & 237 deletions

File tree

.librarian/generator-input/client-post-processing/spanner-integration.yaml

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,22 @@ replacements:
122122
\g<3>\g<4>
123123
count: 1
124124
- paths: [packages/google-cloud-spanner/setup.py]
125-
before: '(?s)dependencies = \[.*?\]\nextras = \{\}'
125+
before: '(?s)dependencies = \[.*?\]\nextras = \{\s*\}'
126126
after: |
127127
dependencies = [
128-
"google-api-core[grpc] >= 1.34.0, <3.0.0,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*",
129-
"google-cloud-core >= 1.4.4, < 3.0.0",
128+
"google-api-core[grpc] >= 2.17.1, <3.0.0",
129+
# Exclude incompatible versions of `google-auth`
130+
# See https://github.com/googleapis/google-cloud-python/issues/12364
131+
"google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0",
132+
"google-cloud-core >= 2.0.0, < 3.0.0",
133+
"grpcio >= 1.49.1, < 2.0.0",
134+
"grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'",
130135
"grpc-google-iam-v1 >= 0.12.4, <1.0.0",
131-
"proto-plus >= 1.22.0, <2.0.0",
132-
"sqlparse >= 0.4.4",
133-
"proto-plus >= 1.22.2, <2.0.0; python_version>='3.11'",
136+
"proto-plus >= 1.22.3, <2.0.0",
137+
"proto-plus >= 1.25.0, <2.0.0; python_version >= '3.13'",
134138
"protobuf >= 4.25.8, < 8.0.0",
135139
"grpc-interceptor >= 0.15.4",
140+
"sqlparse >= 0.4.4",
136141
# Make OpenTelemetry a core dependency
137142
"opentelemetry-api >= 1.22.0",
138143
"opentelemetry-sdk >= 1.22.0",
@@ -601,6 +606,40 @@ replacements:
601606
602607
Next Steps
603608
count: 1
609+
610+
- paths: [
611+
packages/google-cloud-spanner/README.rst
612+
]
613+
before: |
614+
Supported Python Versions
615+
\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^
616+
Our client libraries are compatible with all current `active`_ and `maintenance`_ versions of
617+
Python\.
618+
619+
Python >= 3\.9, including 3\.14
620+
621+
\.\. _active: https://devguide\.python\.org/devcycle/#in-development-main-branch
622+
\.\. _maintenance: https://devguide\.python\.org/devcycle/#maintenance-branches
623+
624+
Unsupported Python Versions
625+
\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^
626+
Python <= 3\.8
627+
after: |
628+
Supported Python Versions
629+
^^^^^^^^^^^^^^^^^^^^^^^^^
630+
Our client libraries are compatible with all current `active`_ and `maintenance`_ versions of
631+
Python.
632+
633+
Python >= 3.10, including 3.14
634+
635+
.. _active: https://devguide.python.org/devcycle/#in-development-main-branch
636+
.. _maintenance: https://devguide.python.org/devcycle/#maintenance-branches
637+
638+
Unsupported Python Versions
639+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
640+
Python <= 3.9
641+
count: 1
642+
604643
# Note: noxfile.py is heavily customized so we clobber the whole file.
605644
- paths: [
606645
packages/google-cloud-spanner/noxfile.py
@@ -627,7 +666,6 @@ replacements:
627666
SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.12"]
628667
629668
ALL_PYTHON: List[str] = [
630-
"3.9",
631669
"3.10",
632670
"3.11",
633671
"3.12",
@@ -667,7 +705,6 @@ replacements:
667705
CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
668706
669707
nox.options.sessions = [
670-
"unit-3.9",
671708
"unit-3.10",
672709
"unit-3.11",
673710
"unit-3.12",
@@ -816,8 +853,6 @@ replacements:
816853
def unit(session, protobuf_implementation):
817854
# Install all test dependencies, then install this package in-place.
818855
819-
if session.python in ("3.7",):
820-
session.skip("Python 3.7 is no longer supported")
821856
if protobuf_implementation == "cpp" and session.python in (
822857
"3.11",
823858
"3.12",
@@ -1379,4 +1414,27 @@ replacements:
13791414
)
13801415
def core_deps_from_source(session, protobuf_implementation):
13811416
"""Run all tests with core dependencies installed from source,
1417+
count: 1
1418+
- paths: [packages/google-cloud-spanner/testing/constraints-3.10.txt]
1419+
before: '(?s)protobuf==4.25.8\n(?!google-cloud-core)'
1420+
after: |
1421+
protobuf==4.25.8
1422+
google-cloud-core==2.0.0
1423+
grpc-google-iam-v1==0.12.4
1424+
sqlparse==0.4.4
1425+
grpc-interceptor==0.15.4
1426+
opentelemetry-api==1.22.0
1427+
opentelemetry-sdk==1.22.0
1428+
opentelemetry-semantic-conventions==0.43b0
1429+
opentelemetry-resourcedetector-gcp==1.8.0a0
1430+
google-cloud-monitoring==2.16.0
1431+
mmh3==4.1.0
1432+
libcst==0.2.5
1433+
googleapis-common-protos==1.60.0
1434+
count: 1
1435+
- paths: [packages/google-cloud-spanner/testing/constraints-3.10.txt]
1436+
before: 'grpcio==1\.44\.0\n(?!grpcio-status)'
1437+
after: |
1438+
grpcio==1.49.1
1439+
grpcio-status==1.49.1
13821440
count: 1

librarian.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1906,7 +1906,6 @@ libraries:
19061906
- docs/spanner_v1/table.rst
19071907
- docs/spanner_v1/transaction.rst
19081908
- tests/unit/gapic/conftest.py
1909-
skip_generate: true
19101909
python:
19111910
library_type: GAPIC_COMBO
19121911
opt_args_by_api:

packages/google-cloud-spanner/.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "Python 3",
55
"build": {
66
// Sets the run context to one level up instead of the .devcontainer folder.
7-
"args": { "VARIANT": "3.8" },
7+
"args": { "VARIANT": "3.10" },
88
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
99
"dockerfile": "Dockerfile"
1010
},

packages/google-cloud-spanner/.devcontainer/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# This file is autogenerated by pip-compile with Python 3.8
2+
# This file is autogenerated by pip-compile with Python 3.8 # version-scanner: ignore
33
# by the following command:
44
#
55
# pip-compile --generate-hashes requirements.in

packages/google-cloud-spanner/docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright 2025 Google LLC
2+
# Copyright 2026 Google LLC
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -83,7 +83,7 @@
8383

8484
# General information about the project.
8585
project = "google-cloud-spanner"
86-
copyright = "2025, Google, LLC"
86+
copyright = "2026, Google, LLC"
8787
author = "Google APIs"
8888

8989
# The version info for the project you're documenting, acts as replacement for

packages/google-cloud-spanner/google/cloud/aio/_cross_sync/cross_sync.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ async def async_func(self, arg: int) -> int:
4242
import concurrent.futures
4343
import inspect
4444
import queue
45-
import sys
4645
import threading
4746
import time
4847
import typing
@@ -269,7 +268,7 @@ def create_task(
269268
sync_executor: ThreadPoolExecutor to use for sync operations. Ignored in async version
270269
"""
271270
task: CrossSync.Task[T] = asyncio.create_task(fn(*fn_args, **fn_kwargs))
272-
if task_name and sys.version_info >= (3, 8):
271+
if task_name:
273272
task.set_name(task_name)
274273
return task
275274

packages/google-cloud-spanner/google/cloud/spanner_admin_database_v1/__init__.py

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright 2025 Google LLC
2+
# Copyright 2026 Google LLC
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121

2222
__version__ = package_version.__version__
2323

24+
from importlib import metadata
2425

2526
from .services.database_admin import DatabaseAdminAsyncClient, DatabaseAdminClient
2627
from .types.backup import (
@@ -99,18 +100,81 @@
99100
api_core.check_python_version("google.cloud.spanner_admin_database_v1") # type: ignore
100101
api_core.check_dependency_versions("google.cloud.spanner_admin_database_v1") # type: ignore
101102
else: # pragma: NO COVER
102-
import warnings
103+
# An older version of api_core is installed which does not define the
104+
# functions above. We do equivalent checks manually.
105+
try:
106+
import warnings
103107

104-
_py_version_str = sys.version.split()[0]
105-
# version-scanner: ignore-next-line
106-
if sys.version_info < (3, 10):
108+
_py_version_str = sys.version.split()[0]
109+
_package_label = "google.cloud.spanner_admin_database_v1"
110+
if sys.version_info < (3, 10):
111+
warnings.warn(
112+
"You are using a non-supported Python version "
113+
+ f"({_py_version_str}). Google will not post any further "
114+
+ f"updates to {_package_label} supporting this Python version. "
115+
+ "Please upgrade to the latest Python version, or at "
116+
+ f"least to Python 3.10, and then update {_package_label}.",
117+
FutureWarning,
118+
)
119+
120+
def parse_version_to_tuple(version_string: str):
121+
"""Safely converts a semantic version string to a comparable tuple of integers.
122+
Example: "4.25.8" -> (4, 25, 8)
123+
Ignores non-numeric parts and handles common version formats.
124+
Args:
125+
version_string: Version string in the format "x.y.z" or "x.y.z<suffix>"
126+
Returns:
127+
Tuple of integers for the parsed version string.
128+
"""
129+
parts = []
130+
for part in version_string.split("."):
131+
try:
132+
parts.append(int(part))
133+
except ValueError:
134+
# If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here.
135+
# This is a simplification compared to 'packaging.parse_version', but sufficient
136+
# for comparing strictly numeric semantic versions.
137+
break
138+
return tuple(parts)
139+
140+
def _get_version(dependency_name):
141+
try:
142+
version_string: str = metadata.version(dependency_name)
143+
parsed_version = parse_version_to_tuple(version_string)
144+
return (parsed_version, version_string)
145+
except Exception:
146+
# Catch exceptions from metadata.version() (e.g., PackageNotFoundError)
147+
# or errors during parse_version_to_tuple
148+
return (None, "--")
149+
150+
_dependency_package = "google.protobuf"
151+
_next_supported_version = "4.25.8"
152+
_next_supported_version_tuple = (4, 25, 8)
153+
_recommendation = " (we recommend 6.x)"
154+
(_version_used, _version_used_string) = _get_version(_dependency_package)
155+
if _version_used and _version_used < _next_supported_version_tuple:
156+
warnings.warn(
157+
f"Package {_package_label} depends on "
158+
+ f"{_dependency_package}, currently installed at version "
159+
+ f"{_version_used_string}. Future updates to "
160+
+ f"{_package_label} will require {_dependency_package} at "
161+
+ f"version {_next_supported_version} or higher{_recommendation}."
162+
+ " Please ensure "
163+
+ "that either (a) your Python environment doesn't pin the "
164+
+ f"version of {_dependency_package}, so that updates to "
165+
+ f"{_package_label} can require the higher version, or "
166+
+ "(b) you manually update your Python environment to use at "
167+
+ f"least version {_next_supported_version} of "
168+
+ f"{_dependency_package}.",
169+
FutureWarning,
170+
)
171+
except Exception:
107172
warnings.warn(
108-
"You are using a non-supported Python version "
109-
+ f"({_py_version_str}). Google will not post any further "
110-
+ "updates to google.cloud.spanner_admin_database_v1 supporting this Python version. "
111-
+ "Please upgrade to the latest Python version, or at "
112-
+ "least to Python 3.10, and then update google.cloud.spanner_admin_database_v1.",
113-
FutureWarning,
173+
"Could not determine the version of Python "
174+
+ "currently being used. To continue receiving "
175+
+ "updates for {_package_label}, ensure you are "
176+
+ "using a supported version of Python; see "
177+
+ "https://devguide.python.org/versions/"
114178
)
115179

116180
__all__ = (

packages/google-cloud-spanner/google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright 2025 Google LLC
2+
# Copyright 2026 Google LLC
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -518,11 +518,11 @@ async def sample_create_database():
518518
)
519519
520520
# Make the request
521-
operation = client.create_database(request=request)
521+
operation = await client.create_database(request=request)
522522
523523
print("Waiting for operation to complete...")
524524
525-
response = (await operation).result()
525+
response = await operation.result()
526526
527527
# Handle the response
528528
print(response)
@@ -814,11 +814,11 @@ async def sample_update_database():
814814
)
815815
816816
# Make the request
817-
operation = client.update_database(request=request)
817+
operation = await client.update_database(request=request)
818818
819819
print("Waiting for operation to complete...")
820820
821-
response = (await operation).result()
821+
response = await operation.result()
822822
823823
# Handle the response
824824
print(response)
@@ -964,11 +964,11 @@ async def sample_update_database_ddl():
964964
)
965965
966966
# Make the request
967-
operation = client.update_database_ddl(request=request)
967+
operation = await client.update_database_ddl(request=request)
968968
969969
print("Waiting for operation to complete...")
970970
971-
response = (await operation).result()
971+
response = await operation.result()
972972
973973
# Handle the response
974974
print(response)
@@ -1778,11 +1778,11 @@ async def sample_create_backup():
17781778
)
17791779
17801780
# Make the request
1781-
operation = client.create_backup(request=request)
1781+
operation = await client.create_backup(request=request)
17821782
17831783
print("Waiting for operation to complete...")
17841784
1785-
response = (await operation).result()
1785+
response = await operation.result()
17861786
17871787
# Handle the response
17881788
print(response)
@@ -1944,11 +1944,11 @@ async def sample_copy_backup():
19441944
)
19451945
19461946
# Make the request
1947-
operation = client.copy_backup(request=request)
1947+
operation = await client.copy_backup(request=request)
19481948
19491949
print("Waiting for operation to complete...")
19501950
1951-
response = (await operation).result()
1951+
response = await operation.result()
19521952
19531953
# Handle the response
19541954
print(response)
@@ -2602,11 +2602,11 @@ async def sample_restore_database():
26022602
)
26032603
26042604
# Make the request
2605-
operation = client.restore_database(request=request)
2605+
operation = await client.restore_database(request=request)
26062606
26072607
print("Waiting for operation to complete...")
26082608
2609-
response = (await operation).result()
2609+
response = await operation.result()
26102610
26112611
# Handle the response
26122612
print(response)

0 commit comments

Comments
 (0)