Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3ffe30e
feat: Add Python 3.14 support
google-labs-jules[bot] Oct 10, 2025
1e89dbc
feat: Add Python 3.14 support
google-labs-jules[bot] Oct 10, 2025
ae8e7fb
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 10, 2025
2d915c6
feat: Add Python 3.14 support
google-labs-jules[bot] Oct 10, 2025
abc3a7a
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 10, 2025
d8cd0a3
feat: Add Python 3.14 support and drop Python 3.7/3.8
google-labs-jules[bot] Oct 13, 2025
3c2a458
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 13, 2025
81a33c1
Update .kokoro/samples/python3.14/common.cfg
chalmerlowe Oct 13, 2025
23dc8d3
feat: Add Python 3.14 support and drop Python 3.7/3.8
google-labs-jules[bot] Oct 15, 2025
583179c
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 15, 2025
2479c2b
feat: Add Python 3.14 support
google-labs-jules[bot] Oct 15, 2025
49e15a2
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 15, 2025
61b301d
feat: Add Python 3.14 support and remove 3.7/3.8 from nox
google-labs-jules[bot] Oct 17, 2025
d710446
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 17, 2025
e83dbf1
feat: Add Python 3.14 support and fix nox session
google-labs-jules[bot] Oct 20, 2025
bf41dee
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 20, 2025
c5272c4
feat: Add Python 3.14 support and update CI
google-labs-jules[bot] Oct 20, 2025
69233d1
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 20, 2025
43b1b51
feat: Add Python 3.14 support and fix scheduler shutdown
google-labs-jules[bot] Oct 20, 2025
ee497e7
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 20, 2025
760c06c
feat: Add Python 3.14 support and fix nox session
google-labs-jules[bot] Oct 20, 2025
dbecfe8
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 20, 2025
b90685b
Apply suggestion from @chalmerlowe
chalmerlowe Oct 20, 2025
e60937f
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Oct 20, 2025
0fa4f69
updates type hint to accound for 3.14 changes and updates tests now t…
chalmerlowe Oct 20, 2025
8880472
edits to correct for failing tests
chalmerlowe Oct 21, 2025
2f3e30a
experimenting with time limits for threading.Barrier
chalmerlowe Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/sync-repo-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ branchProtectionRules:
- 'Samples - Python 3.10'
- 'Samples - Python 3.11'
- 'Samples - Python 3.12'
- 'Samples - Python 3.14'
- 'OwlBot Post Processor'
- 'docs'
- 'docfx'
Expand All @@ -27,4 +28,5 @@ branchProtectionRules:
- 'unit (3.10)'
- 'unit (3.11)'
- 'unit (3.12)'
- 'unit (3.14)'
- 'cover'
7 changes: 2 additions & 5 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,34 @@
name: unittest
jobs:
unit:
# TODO(https://github.com/googleapis/gapic-generator-python/issues/2303): use `ubuntu-latest` once this bug is fixed.
# Use ubuntu-22.04 until Python 3.7 is removed from the test matrix
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install nox
run: |
python -m pip install --upgrade setuptools pip wheel
python -m pip install nox
- name: Run unit tests
env:
COVERAGE_FILE: .coverage-${{ matrix.python }}
run: |
nox -s unit-${{ matrix.python }}
- name: Upload coverage results
uses: actions/upload-artifact@v4
with:
name: coverage-artifact-${{ matrix.python }}
path: .coverage-${{ matrix.python }}
include-hidden-files: true

cover:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-latest
needs:
- unit
Expand Down
40 changes: 40 additions & 0 deletions .kokoro/samples/python3.14/common.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Format: //devtools/kokoro/config/proto/build.proto

# Build logs will be here
action {
define_artifacts {
regex: "**/*sponge_log.xml"
}
}

# Specify which tests to run
env_vars: {
key: "RUN_TESTS_SESSION"
value: "py-3.14"
}

# Declare build specific Cloud project.
env_vars: {
key: "BUILD_SPECIFIC_GCLOUD_PROJECT"
value: "python-docs-samples-tests-314"
}

env_vars: {
key: "TRAMPOLINE_BUILD_FILE"
value: "github/python-pubsub/.kokoro/test-samples.sh"
}

# Configure the docker image for kokoro-trampoline.
env_vars: {
key: "TRAMPOLINE_IMAGE"
value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
}

# Download secrets for samples
gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"

# Download trampoline resources.
gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"

# Use the trampoline script to run in docker.
build_file: "python-pubsub/.kokoro/trampoline_v2.sh"
6 changes: 6 additions & 0 deletions .kokoro/samples/python3.14/continuous.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Format: //devtools/kokoro/config/proto/build.proto

env_vars: {
key: "INSTALL_LIBRARY_FROM_SOURCE"
value: "True"
}
11 changes: 11 additions & 0 deletions .kokoro/samples/python3.14/periodic-head.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Format: //devtools/kokoro/config/proto/build.proto

env_vars: {
key: "INSTALL_LIBRARY_FROM_SOURCE"
value: "True"
}

env_vars: {
key: "TRAMPOLINE_BUILD_FILE"
value: "github/python-pubsub/.kokoro/test-samples-against-head.sh"
}
6 changes: 6 additions & 0 deletions .kokoro/samples/python3.14/periodic.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Format: //devtools/kokoro/config/proto/build.proto

env_vars: {
key: "INSTALL_LIBRARY_FROM_SOURCE"
value: "False"
}
6 changes: 6 additions & 0 deletions .kokoro/samples/python3.14/presubmit.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Format: //devtools/kokoro/config/proto/build.proto

env_vars: {
key: "INSTALL_LIBRARY_FROM_SOURCE"
value: "True"
}
14 changes: 6 additions & 8 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ In order to add a feature:
documentation.

- The feature must work fully on the following CPython versions:
3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13 on both UNIX and Windows.
3.9, 3.10, 3.11, 3.12, 3.13, 3.14 on both UNIX and Windows.

- The feature must not add unnecessary dependencies (where
"unnecessary" is of course subjective, but new dependencies should
Expand Down Expand Up @@ -195,11 +195,11 @@ configure them just like the System Tests.

# Run all tests in a folder
$ cd samples/snippets
$ nox -s py-3.8
$ nox -s py-3.9

# Run a single sample test
$ cd samples/snippets
$ nox -s py-3.8 -- -k <name of test>
$ nox -s py-3.9 -- -k <name of test>

********************************************
Note About ``README`` as it pertains to PyPI
Expand All @@ -221,29 +221,27 @@ Supported Python Versions

We support:

- `Python 3.7`_
- `Python 3.8`_
- `Python 3.9`_
- `Python 3.10`_
- `Python 3.11`_
- `Python 3.12`_
- `Python 3.13`_
- `Python 3.14`_

.. _Python 3.7: https://docs.python.org/3.7/
.. _Python 3.8: https://docs.python.org/3.8/
.. _Python 3.9: https://docs.python.org/3.9/
.. _Python 3.10: https://docs.python.org/3.10/
.. _Python 3.11: https://docs.python.org/3.11/
.. _Python 3.12: https://docs.python.org/3.12/
.. _Python 3.13: https://docs.python.org/3.13/
.. _Python 3.14: https://docs.python.org/3.14/


Supported versions can be found in our ``noxfile.py`` `config`_.

.. _config: https://github.com/googleapis/python-pubsub/blob/main/noxfile.py


We also explicitly decided to support Python 3 beginning with version 3.7.
We also explicitly decided to support Python 3 beginning with version 3.9.
Reasons for this include:

- Encouraging use of newest versions of Python 3
Expand Down
9 changes: 7 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@ dependencies.

Supported Python Versions
^^^^^^^^^^^^^^^^^^^^^^^^^
Python >= 3.7
- Python 3.9
- Python 3.10
- Python 3.11
- Python 3.12
- Python 3.13
- Python 3.14

Deprecated Python Versions
^^^^^^^^^^^^^^^^^^^^^^^^^^
Python <= 3.6.
Python <= 3.8.

The last version of this library compatible with Python 2.7 is google-cloud-pubsub==1.7.0.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ def dispatcher(self) -> Optional[dispatcher.Dispatcher]:
return self._dispatcher

@property
def leaser(self) -> Optional[leaser.Leaser]:
def leaser(self) -> "leaser.Leaser | None":
"""The leaser helper."""
return self._leaser

Expand Down Expand Up @@ -1041,13 +1041,10 @@ def _shutdown(self, reason: Any = None) -> None:
assert self._leaser is not None
self._leaser.stop()

total = len(dropped_messages) + len(
self._messages_on_hold._messages_on_hold
)
on_hold_msgs = self._messages_on_hold._messages_on_hold
total = len(dropped_messages) + len(on_hold_msgs)
_LOGGER.debug(f"NACK-ing all not-yet-dispatched messages (total: {total}).")
messages_to_nack = itertools.chain(
dropped_messages, self._messages_on_hold._messages_on_hold
)
messages_to_nack = itertools.chain(dropped_messages, on_hold_msgs)
for msg in messages_to_nack:
msg.nack()

Expand Down
24 changes: 5 additions & 19 deletions google/cloud/pubsub_v1/subscriber/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Scheduler(metaclass=abc.ABCMeta):

@property
@abc.abstractmethod
def queue(self) -> queue.Queue: # pragma: NO COVER
def queue(self) -> "queue.Queue": # pragma: NO COVER
"""Queue: A concurrency-safe queue specific to the underlying
concurrency implementation.

Expand Down Expand Up @@ -150,21 +150,7 @@ def shutdown(
It is assumed that each message was submitted to the scheduler as the
first positional argument to the provided callback.
"""
dropped_messages = []

# Drop all pending item from the executor. Without this, the executor will also
# try to process any pending work items before termination, which is undesirable.
#
# TODO: Replace the logic below by passing `cancel_futures=True` to shutdown()
# once we only need to support Python 3.9+.
try:
while True:
work_item = self._executor._work_queue.get(block=False)
if work_item is None: # Exceutor in shutdown mode.
continue
dropped_messages.append(work_item.args[0]) # type: ignore[index]
except queue.Empty:
pass

self._executor.shutdown(wait=await_msg_callbacks)
return dropped_messages
# The public API for ThreadPoolExecutor does not allow retrieving pending
# work items, so return an empty list.
self._executor.shutdown(wait=await_msg_callbacks, cancel_futures=True)
return []
25 changes: 16 additions & 9 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,12 @@
DEFAULT_PYTHON_VERSION = "3.13"

UNIT_TEST_PYTHON_VERSIONS: List[str] = [
"3.7",
"3.8",
"3.9",
"3.10",
"3.11",
"3.12",
"3.13",
"3.14",
]
UNIT_TEST_STANDARD_DEPENDENCIES = [
"mock",
Expand Down Expand Up @@ -234,7 +233,12 @@ def install_unittest_dependencies(session, *constraints):
def unit(session, protobuf_implementation):
# Install all test dependencies, then install this package in-place.

if protobuf_implementation == "cpp" and session.python in ("3.11", "3.12", "3.13"):
if protobuf_implementation == "cpp" and session.python in (
"3.11",
"3.12",
"3.13",
"3.14",
):
session.skip("cpp implementation is not supported in python 3.11+")

constraints_path = str(
Expand Down Expand Up @@ -436,15 +440,20 @@ def docfx(session):
)


@nox.session(python="3.13")
@nox.session(python="3.14")
@nox.parametrize(
"protobuf_implementation",
["python", "upb", "cpp"],
)
def prerelease_deps(session, protobuf_implementation):
"""Run all tests with prerelease versions of dependencies installed."""

if protobuf_implementation == "cpp" and session.python in ("3.11", "3.12", "3.13"):
if protobuf_implementation == "cpp" and session.python in (
"3.11",
"3.12",
"3.13",
"3.14",
):
session.skip("cpp implementation is not supported in python 3.11+")

# Install all dependencies
Expand All @@ -470,12 +479,10 @@ def prerelease_deps(session, protobuf_implementation):
# Ignore leading whitespace and comment lines.
constraints_deps = [
match.group(1)
for match in re.finditer(
r"^\s*(\S+)(?===\S+)", constraints_text, flags=re.MULTILINE
)
for match in re.finditer(r"^\s*(\S+)", constraints_text, flags=re.MULTILINE)
]

session.install(*constraints_deps)
# session.install(*constraints_deps)

prerel_deps = [
"protobuf",
Expand Down
2 changes: 1 addition & 1 deletion owlbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@
samples=True,
cov_level=99,
versions=gcp.common.detect_versions(path="./google", default_first=True),
unit_test_python_versions=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"],
unit_test_python_versions=["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"],
unit_test_dependencies=["flaky"],
system_test_python_versions=["3.12"],
system_test_external_dependencies=["psutil","flaky"],
Expand Down
8 changes: 4 additions & 4 deletions samples/snippets/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def pubsub_publish_otel_tracing(
topic_path = publisher.topic_path(topic_project_id, topic_id)
# Publish messages.
for n in range(1, 10):
data_str = f"Message number {n}"
data_str = '{"data": "Message number ' + str(n) + '"}'
# Data must be a bytestring
data = data_str.encode("utf-8")
# When you publish a message, the client returns a future.
Expand Down Expand Up @@ -519,7 +519,7 @@ def publish_messages(project_id: str, topic_id: str) -> None:
topic_path = publisher.topic_path(project_id, topic_id)

for n in range(1, 10):
data_str = f"Message number {n}"
data_str = '{"data": "Message number ' + str(n) + '"}'
# Data must be a bytestring
data = data_str.encode("utf-8")
# When you publish a message, the client returns a future.
Expand All @@ -545,7 +545,7 @@ def publish_messages_with_custom_attributes(project_id: str, topic_id: str) -> N
topic_path = publisher.topic_path(project_id, topic_id)

for n in range(1, 10):
data_str = f"Message number {n}"
data_str = '{"data": "Message number ' + str(n) + '"}'
# Data must be a bytestring
data = data_str.encode("utf-8")
# Add two attributes, origin and username, to the message
Expand Down Expand Up @@ -627,7 +627,7 @@ def callback(future: pubsub_v1.publisher.futures.Future) -> None:
print(message_id)

for n in range(1, 10):
data_str = f"Message number {n}"
data_str = '{"data": "Message number ' + str(n) + '"}'
# Data must be a bytestring
data = data_str.encode("utf-8")
publish_future = publisher.publish(topic_path, data)
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
release_status = "Development Status :: 5 - Production/Stable"

dependencies = [
"grpcio >= 1.51.3, < 2.0.0", # https://github.com/googleapis/python-pubsub/issues/609
"grpcio >= 1.51.3, < 2.0.0; python_version < '3.14'", # https://github.com/googleapis/python-pubsub/issues/609
"grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'",
# google-api-core >= 1.34.0 is allowed in order to support google-api-core 1.x
"google-auth >= 2.14.1, <3.0.0",
"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.*",
Expand Down Expand Up @@ -88,6 +89,7 @@
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Topic :: Internet",
],
Expand Down
1 change: 1 addition & 0 deletions testing/constraints-3.14.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
grpcio >= 1.75.1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed in the constraints file? I thought since it's in the setup.py, we'd expect this to be constrained by default.

If I understand correctly, having this as an explicit constraint might actually make us miss bugs, because we're now installing the library with a non-default configuration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To your point:

having this as an explicit constraint might actually make us miss bugs, because we're now installing the library with a non-default configuration

First, with a broad range of allowable dependencies in setup.py (which we want as a library others will consume) we have no control over what is a default configuration. The pip resolver could choose any number of combinations of dependencies. There is no such thing as a default unless you pin to exact versions.

Second, the requirement that "grpcio >= 1.75.1 is the same lower bound in setup.py and in constraints.txt and it is used only for cases where Python runtime 3.14 is installed, because this limitation is in the constraints-3.14.txt file and because we have conditional logic in the setup.py file to check for runtimes. In this case, the lower bounds are identical. Often constraints pin very specific versions of dependencies, but we have chosen to put a lower bound instead of an exact pin.

Thus, I could go either way (keep the constraint OR ditch it).

For context: Pip resolves dependencies per requirements in setup.py (or pyproject.toml) and as a widely used library we strive to keep that range of available dependencies as wide as possible to ensure we minimize dependency conflicts. Constraints files do NOT impact builds unless the user specifically tells pip to use them in the resolution process by passing in the --constraints or -c argument (i.e. pip install my_package -c constraints.txt). I acknowledge that we currently capture the need for a version >= x in the setup.py file so theoretically we don't need to include anything in a constraints.txt.

However: having the value in the constraints.txt file does several things:

  • It provides the user with a clear picture of known good configurations that we have "tested against" (i.e. in our noxfiles we explicitly tell nox to install using constraints.txt). Now, this would be more precise if we pinned to a specific version in the constraint file, but we aren't doing that here.
  • It serves as a redundancy against unexpected or accidental changes to the ranges delimited in setup.py.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not too worried either way. As you say, the bounds in the constraints and the setup are identical, so it feels like having this here is unnecessary, but not a problem either

It serves as a redundancy against unexpected or accidental changes to the ranges delimited in setup.py.

This situation was the concern I had in mind though. If we accidentally lost the grpc version requirement somehow, the tests would still pass because of this constraint, even though real users would run into issues. It seems safer to have the tests fail in that situation, I don't think we'd want redundancy

2 changes: 1 addition & 1 deletion tests/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ def callback(message):

# The messages that were not processed should have been NACK-ed and we should
# receive them again quite soon.
all_done = threading.Barrier(7 + 1, timeout=5) # +1 because of the main thread
all_done = threading.Barrier(7 + 1, timeout=15) # +1 because of the main thread
remaining = []

def callback2(message):
Expand Down
Loading