Repository: scrapy/scrapy (python)

Issue:
GCSFeedStorage does not support feed_options
Since https://github.com/scrapy/scrapy/issues/6105, `feed_options` parameter needs to be supported by the feed storage classes.
Now with version 2.12.0, GCSFeedStorage does not have this parameter added, therefore it fails upon usage.
```
GCSFeedStorage.from_crawler() got an unexpected keyword argument 'feed_options'
```

Pull request:
fix: added feed_options as a keyword argument to GCSFeedStorage.
This PR attempts to resolve #6617

- Added feed_options as a keyword argument to `GCSFeedStorage` in its `__init__()` and `from_crawler()`.
- Added test cases to `GCSFeedStorageTest`
~~- Refactored class `GCSFeedStorage` to import and create gcs client in its `__init__` method~~
~~- Added `requires_gcs` to the markers list in pyproject.toml~~
~~- Created a fixture to check google-cloud-storage library in GCSFeedStorageTest tests just like checking boto3 in S3FeedStorageTest~~

Patch:
diff --git a/scrapy/extensions/feedexport.py b/scrapy/extensions/feedexport.py
index c6e2aa0dd..8bcd4e40d 100644
--- a/scrapy/extensions/feedexport.py
+++ b/scrapy/extensions/feedexport.py
@@ -276,7 +276,14 @@ class S3FeedStorage(BlockingFeedStorage):
 
 
 class GCSFeedStorage(BlockingFeedStorage):
-    def __init__(self, uri: str, project_id: str | None, acl: str | None):
+    def __init__(
+        self,
+        uri: str,
+        project_id: str | None,
+        acl: str | None,
+        *,
+        feed_options: dict[str, Any] | None = None,
+    ):
         self.project_id: str | None = project_id
         self.acl: str | None = acl
         u = urlparse(uri)
@@ -284,12 +291,26 @@ class GCSFeedStorage(BlockingFeedStorage):
         self.bucket_name: str = u.hostname
         self.blob_name: str = u.path[1:]  # remove first "/"
 
+        if feed_options and feed_options.get("overwrite", True) is False:
+            logger.warning(
+                "GCS does not support appending to files. To "
+                "suppress this warning, remove the overwrite "
+                "option from your FEEDS setting or set it to True."
+            )
+
     @classmethod
-    def from_crawler(cls, crawler: Crawler, uri: str) -> Self:
+    def from_crawler(
+        cls,
+        crawler: Crawler,
+        uri: str,
+        *,
+        feed_options: dict[str, Any] | None = None,
+    ) -> Self:
         return cls(
             uri,
             crawler.settings["GCS_PROJECT_ID"],
             crawler.settings["FEED_STORAGE_GCS_ACL"] or None,
+            feed_options=feed_options,
         )
 
     def _store_in_thread(self, file: IO[bytes]) -> None:
diff --git a/tox.ini b/tox.ini
index cf5e19a61..0f91db19d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -118,6 +118,7 @@ setenv =
 install_command =
     python -I -m pip install {opts} {packages}
 commands =
+    ; tests for docs fail with parsel < 1.8.0
     pytest --cov-config=pyproject.toml --cov=scrapy --cov-report=xml --cov-report= {posargs:--durations=10 scrapy tests}
 
 [testenv:pinned]
@@ -191,14 +192,35 @@ setenv =
 [testenv:pypy3]
 basepython = pypy3
 commands =
+    ; not enabling coverage as it significantly increases the run time
     pytest {posargs:--durations=10 docs scrapy tests}
 
+[testenv:pypy3-extra-deps]
+basepython = pypy3
+deps =
+    {[testenv:extra-deps]deps}
+commands = {[testenv:pypy3]commands}
+
 [testenv:pypy3-pinned]
-basepython = pypy3.9
+basepython = pypy3.10
 deps =
-    {[pinned]deps}
+    cryptography==41.0.5
+    cssselect==0.9.1
+    h2==3.1
+    itemadapter==0.1.0
+    parsel==1.5.0
+    Protego==0.1.15
+    pyOpenSSL==23.3.0
+    queuelib==1.4.2
+    service_identity==18.1.0
+    Twisted[http2]==21.7.0
+    w3lib==1.17.0
+    zope.interface==5.1.0
+    lxml==4.6.0
+    {[test-requirements]deps}
     PyPyDispatcher==2.1.0
 commands =
+    ; disabling both coverage and docs tests
     pytest {posargs:--durations=10 scrapy tests}
 install_command = {[pinned]install_command}
 setenv =


Test patch:
diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml
index 89d1e70ac..6c7842217 100644
--- a/.github/workflows/tests-ubuntu.yml
+++ b/.github/workflows/tests-ubuntu.yml
@@ -30,9 +30,6 @@ jobs:
         - python-version: "3.13"
           env:
             TOXENV: asyncio
-        - python-version: pypy3.9
-          env:
-            TOXENV: pypy3
         - python-version: pypy3.10
           env:
             TOXENV: pypy3
@@ -44,7 +41,7 @@ jobs:
         - python-version: 3.9.19
           env:
             TOXENV: asyncio-pinned
-        - python-version: pypy3.9
+        - python-version: pypy3.10
           env:
             TOXENV: pypy3-pinned
         - python-version: 3.9.19
@@ -57,6 +54,9 @@ jobs:
         - python-version: "3.13"
           env:
             TOXENV: extra-deps
+        - python-version: pypy3.10
+          env:
+            TOXENV: pypy3-extra-deps
         - python-version: "3.13"
           env:
             TOXENV: botocore
diff --git a/tests/test_feedexport.py b/tests/test_feedexport.py
index 81d05e2a3..7edffa1f6 100644
--- a/tests/test_feedexport.py
+++ b/tests/test_feedexport.py
@@ -523,6 +523,21 @@ class GCSFeedStorageTest(unittest.TestCase):
             bucket_mock.blob.assert_called_once_with("export.csv")
             blob_mock.upload_from_file.assert_called_once_with(f, predefined_acl=acl)
 
+    def test_overwrite_default(self):
+        with LogCapture() as log:
+            GCSFeedStorage("gs://mybucket/export.csv", "myproject-123", "custom-acl")
+        self.assertNotIn("GCS does not support appending to files", str(log))
+
+    def test_overwrite_false(self):
+        with LogCapture() as log:
+            GCSFeedStorage(
+                "gs://mybucket/export.csv",
+                "myproject-123",
+                "custom-acl",
+                feed_options={"overwrite": False},
+            )
+        self.assertIn("GCS does not support appending to files", str(log))
+
 
 class StdoutFeedStorageTest(unittest.TestCase):
     @defer.inlineCallbacks