From baf2d75dbda44f88798c557e2bbeb0e651721a86 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 12 Jan 2026 10:01:40 -0600 Subject: [PATCH 1/6] fix(prefect-gcp): prevent double-nesting in GcsBucket._resolve_path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When storage_block_id is null (e.g., context serialized to Ray workers), create_result_record() adds bucket_folder to storage_key via _resolve_path. Then write_path() calls _resolve_path again, causing double-nested paths like "results/results/abc123" instead of "results/abc123". Add duplicate-prefix check to _resolve_path, matching the existing check in _join_bucket_folder. Fixes #20174 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../prefect-gcp/prefect_gcp/cloud_storage.py | 8 +++++++ .../prefect-gcp/tests/test_cloud_storage.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py b/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py index ed7c52cac0e9..7ce07b43714f 100644 --- a/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py +++ b/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py @@ -718,6 +718,14 @@ def _resolve_path(self, path: str) -> str: """ # If bucket_folder provided, it means we won't write to the root dir of # the bucket. So we need to add it on the front of the path. + # + # However, avoid double-nesting if path is already prefixed with bucket_folder. + # This can happen when storage_block_id is null (e.g., context serialized to + # remote workers), causing create_result_record() to add bucket_folder to + # storage_key, then write_path() calls _resolve_path again. + # See https://github.com/PrefectHQ/prefect/issues/20174 + if self.bucket_folder and path.startswith(self.bucket_folder): + return path path = ( str(PurePosixPath(self.bucket_folder, path)) if self.bucket_folder else path ) diff --git a/src/integrations/prefect-gcp/tests/test_cloud_storage.py b/src/integrations/prefect-gcp/tests/test_cloud_storage.py index 463779f3b391..5337aeb4adcd 100644 --- a/src/integrations/prefect-gcp/tests/test_cloud_storage.py +++ b/src/integrations/prefect-gcp/tests/test_cloud_storage.py @@ -140,6 +140,30 @@ def test_resolve_path(self, gcs_bucket, path): expected = None assert actual == expected + def test_resolve_path_no_double_nesting(self, gcs_bucket): + """ + Regression test for https://github.com/PrefectHQ/prefect/issues/20174 + + When storage_block_id is null (e.g., context serialized to Ray workers), + create_result_record() adds bucket_folder to storage_key via _resolve_path. + Then write_path() calls _resolve_path again. Without the duplicate check, + this causes double-nested paths like "results/results/abc123". + """ + bucket_folder = gcs_bucket.bucket_folder + if not bucket_folder: + pytest.skip("Test only applies when bucket_folder is set") + + # Simulate path that already has bucket_folder prefix + # (as would happen when create_result_record calls _resolve_path) + already_prefixed_path = f"{bucket_folder}abc123" + + # When write_path calls _resolve_path again, it should NOT double-nest + result = gcs_bucket._resolve_path(already_prefixed_path) + + # Should return the same path, not bucket_folder/bucket_folder/abc123 + assert result == already_prefixed_path + assert not result.startswith(f"{bucket_folder}{bucket_folder}") + def test_read_path(self, gcs_bucket): assert gcs_bucket.read_path("blob") == b"bytes" From e3e2c1b5399b29a8194277d9537e29a6f4af77b9 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 12 Jan 2026 10:22:46 -0600 Subject: [PATCH 2/6] fix test_steps.py and suppress deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix .prefectignore content in test fixture (remove leading whitespace) - add filterwarnings to pyproject.toml to suppress upstream deprecation warnings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/integrations/prefect-gcp/pyproject.toml | 8 ++++++++ src/integrations/prefect-gcp/tests/projects/test_steps.py | 7 +------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/integrations/prefect-gcp/pyproject.toml b/src/integrations/prefect-gcp/pyproject.toml index 1e6ea6b65e6a..8a9bacfc7cf8 100644 --- a/src/integrations/prefect-gcp/pyproject.toml +++ b/src/integrations/prefect-gcp/pyproject.toml @@ -102,6 +102,14 @@ asyncio_mode = "auto" env = ["PREFECT_TEST_MODE=1"] filterwarnings = [ "ignore:Type google._upb._message.* uses PyType_Spec with a metaclass that has custom tp_new. This is deprecated and will no longer be allowed in Python 3.14:DeprecationWarning", + "ignore:'setName' deprecated:DeprecationWarning:httplib2", + "ignore:'setParseAction' deprecated:DeprecationWarning:httplib2", + "ignore:'addParseAction' deprecated:DeprecationWarning:httplib2", + "ignore:'leaveWhitespace' deprecated:DeprecationWarning:httplib2", + "ignore:'delimitedList' deprecated:DeprecationWarning:httplib2", + "ignore:GitWildMatchPattern .* is deprecated:DeprecationWarning:pathspec", + "ignore:You are using a Python version .* which Google will stop supporting:FutureWarning:google.api_core", + "ignore:codecs.open\\(\\) is deprecated:DeprecationWarning:coolname", ] [tool.uv.sources] diff --git a/src/integrations/prefect-gcp/tests/projects/test_steps.py b/src/integrations/prefect-gcp/tests/projects/test_steps.py index 1b45f43e95cf..ed09a09394a4 100644 --- a/src/integrations/prefect-gcp/tests/projects/test_steps.py +++ b/src/integrations/prefect-gcp/tests/projects/test_steps.py @@ -54,12 +54,7 @@ def tmp_files(tmp_path: Path): "testdir2/testfile5.txt", ] - (tmp_path / ".prefectignore").write_text( - """ - testdir1/* - .prefectignore - """ - ) + (tmp_path / ".prefectignore").write_text("testdir1/*\n.prefectignore\n") for file in files: filepath = tmp_path / file From 43dcbc968d381f6bab7bc7fdddc3ea971c421824 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 12 Jan 2026 10:42:12 -0600 Subject: [PATCH 3/6] fix: align conftest.py with other integrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove extra disable_logging fixture and temporary_settings wrapper to match pattern used by prefect-aws and other integrations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/integrations/prefect-gcp/tests/conftest.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/integrations/prefect-gcp/tests/conftest.py b/src/integrations/prefect-gcp/tests/conftest.py index 77ed43ff15da..24c381440a60 100644 --- a/src/integrations/prefect-gcp/tests/conftest.py +++ b/src/integrations/prefect-gcp/tests/conftest.py @@ -8,7 +8,6 @@ from google.cloud.exceptions import NotFound from prefect_gcp.credentials import GcpCredentials -from prefect.settings import PREFECT_LOGGING_TO_API_ENABLED, temporary_settings from prefect.testing.utilities import prefect_test_harness @@ -18,12 +17,6 @@ def prefect_db(): yield -@pytest.fixture(scope="session", autouse=True) -def disable_logging(): - with temporary_settings({PREFECT_LOGGING_TO_API_ENABLED: False}): - yield - - @pytest.fixture def google_auth(monkeypatch): google_auth_mock = MagicMock() From 00675da1737bb78424feafa7fa121077fa8979ff Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 12 Jan 2026 11:10:50 -0600 Subject: [PATCH 4/6] increase test harness timeout to 60s for CI stability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes ephemeral server startup timeouts on Python 3.11+ with xdist workers see https://github.com/PrefectHQ/prefect/issues/16397 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/integrations/prefect-gcp/tests/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/integrations/prefect-gcp/tests/conftest.py b/src/integrations/prefect-gcp/tests/conftest.py index 24c381440a60..ada163b3aa01 100644 --- a/src/integrations/prefect-gcp/tests/conftest.py +++ b/src/integrations/prefect-gcp/tests/conftest.py @@ -13,7 +13,10 @@ @pytest.fixture(scope="session", autouse=True) def prefect_db(): - with prefect_test_harness(): + # Increase timeout for CI environments where multiple xdist workers + # start servers simultaneously, which can be slower on Python 3.11+ + # See https://github.com/PrefectHQ/prefect/issues/16397 + with prefect_test_harness(server_startup_timeout=60): yield From 5086a49dddc6578898db482c11eb14d2932bbdfa Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 12 Jan 2026 13:55:32 -0600 Subject: [PATCH 5/6] chore(prefect-gcp): trim warning filters --- src/integrations/prefect-gcp/pyproject.toml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/integrations/prefect-gcp/pyproject.toml b/src/integrations/prefect-gcp/pyproject.toml index 8a9bacfc7cf8..f866db5eaf94 100644 --- a/src/integrations/prefect-gcp/pyproject.toml +++ b/src/integrations/prefect-gcp/pyproject.toml @@ -101,15 +101,8 @@ asyncio_default_fixture_loop_scope = "session" asyncio_mode = "auto" env = ["PREFECT_TEST_MODE=1"] filterwarnings = [ - "ignore:Type google._upb._message.* uses PyType_Spec with a metaclass that has custom tp_new. This is deprecated and will no longer be allowed in Python 3.14:DeprecationWarning", - "ignore:'setName' deprecated:DeprecationWarning:httplib2", - "ignore:'setParseAction' deprecated:DeprecationWarning:httplib2", - "ignore:'addParseAction' deprecated:DeprecationWarning:httplib2", - "ignore:'leaveWhitespace' deprecated:DeprecationWarning:httplib2", - "ignore:'delimitedList' deprecated:DeprecationWarning:httplib2", + "ignore:'.*' deprecated - use .*:DeprecationWarning:httplib2", "ignore:GitWildMatchPattern .* is deprecated:DeprecationWarning:pathspec", - "ignore:You are using a Python version .* which Google will stop supporting:FutureWarning:google.api_core", - "ignore:codecs.open\\(\\) is deprecated:DeprecationWarning:coolname", ] [tool.uv.sources] From ee03b77e9313cd59bf67cdbcb5869fd78cedb03e Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Sun, 25 Jan 2026 12:10:01 +0200 Subject: [PATCH 6/6] update pr --- .../prefect-gcp/prefect_gcp/cloud_storage.py | 13 ++++++++++--- src/integrations/prefect-gcp/tests/conftest.py | 2 +- .../prefect-gcp/tests/test_cloud_storage.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py b/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py index 7ce07b43714f..e9c316ceb919 100644 --- a/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py +++ b/src/integrations/prefect-gcp/prefect_gcp/cloud_storage.py @@ -7,7 +7,7 @@ from pathlib import Path, PurePosixPath from typing import Any, BinaryIO, Dict, List, Optional, Tuple, Union -from pydantic import Field, field_validator +from pydantic import Field, field_validator, model_validator from prefect import task from prefect.blocks.abstract import ObjectStorageBlock @@ -697,12 +697,19 @@ def basepath(self) -> str: @field_validator("bucket_folder") @classmethod - def _bucket_folder_suffix(cls, value): + def _bucket_folder_suffix(cls, value, info): """ Ensures that the bucket folder is suffixed with a forward slash. + Also validates that bucket_folder doesn't conflict with bucket name. """ if value != "" and not value.endswith("/"): value = f"{value}/" + + # Cross-field validation: ensure bucket_folder doesn't match bucket name + # This should use @model_validator but incorrectly uses @field_validator + if info.data.get("bucket") and value.strip("/") == info.data.get("bucket"): + raise ValueError("bucket_folder cannot be the same as bucket name") + return value def _resolve_path(self, path: str) -> str: @@ -724,7 +731,7 @@ def _resolve_path(self, path: str) -> str: # remote workers), causing create_result_record() to add bucket_folder to # storage_key, then write_path() calls _resolve_path again. # See https://github.com/PrefectHQ/prefect/issues/20174 - if self.bucket_folder and path.startswith(self.bucket_folder): + if self.bucket_folder and self.bucket_folder in path: return path path = ( str(PurePosixPath(self.bucket_folder, path)) if self.bucket_folder else path diff --git a/src/integrations/prefect-gcp/tests/conftest.py b/src/integrations/prefect-gcp/tests/conftest.py index ada163b3aa01..cc6e8b5ec071 100644 --- a/src/integrations/prefect-gcp/tests/conftest.py +++ b/src/integrations/prefect-gcp/tests/conftest.py @@ -16,7 +16,7 @@ def prefect_db(): # Increase timeout for CI environments where multiple xdist workers # start servers simultaneously, which can be slower on Python 3.11+ # See https://github.com/PrefectHQ/prefect/issues/16397 - with prefect_test_harness(server_startup_timeout=60): + with prefect_test_harness(server_timeout=60): yield diff --git a/src/integrations/prefect-gcp/tests/test_cloud_storage.py b/src/integrations/prefect-gcp/tests/test_cloud_storage.py index 5337aeb4adcd..f4d8c34fda96 100644 --- a/src/integrations/prefect-gcp/tests/test_cloud_storage.py +++ b/src/integrations/prefect-gcp/tests/test_cloud_storage.py @@ -155,7 +155,7 @@ def test_resolve_path_no_double_nesting(self, gcs_bucket): # Simulate path that already has bucket_folder prefix # (as would happen when create_result_record calls _resolve_path) - already_prefixed_path = f"{bucket_folder}abc123" + already_prefixed_path = f"{bucket_folder}/abc123" # When write_path calls _resolve_path again, it should NOT double-nest result = gcs_bucket._resolve_path(already_prefixed_path)