Repository: scrapy/scrapy
Pull request #6719 (2025-03-11 10:30:15)

Remove or post-pone the use of itemadapter.is_item, as a potentially expensive call
See https://github.com/scrapy/itemadapter/issues/59.

Split off from https://github.com/scrapy/scrapy/pull/6715 and extended.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) Report
Attention: Patch coverage is `11.11111%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 33.24%. Comparing base [(`2accaa4`)](https://app.codecov.io/gh/scrapy/scrapy/commit/2accaa4af4ac3c53172ec2a30131c351555526c9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) to head [(`ba5a1b5`)](https://app.codecov.io/gh/scrapy/scrapy/commit/ba5a1b54b314188301f8f2549b3d3a323ba3693d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy).

| [Files with missing lines](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) | Patch % | Lines |
|---|---|---|
| [scrapy/commands/parse.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fcommands%2Fparse.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvbW1hbmRzL3BhcnNlLnB5) | 33.33% | [2 Missing :warning: ](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) |
| [scrapy/exporters.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fexporters.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2V4cG9ydGVycy5weQ==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) |
| [scrapy/utils/serialize.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Futils%2Fserialize.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L3V0aWxzL3NlcmlhbGl6ZS5weQ==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) |
| [scrapy/core/engine.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fcore%2Fengine.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvZW5naW5lLnB5) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) |
| [scrapy/core/scraper.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fcore%2Fscraper.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvc2NyYXBlci5weQ==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master    #6719       +/-   ##
===========================================
- Coverage   90.66%   33.24%   -57.42%     
===========================================
  Files         162      162               
  Lines       12129    12121        -8     
  Branches     1568     1565        -3     
===========================================
- Hits        10997     4030     -6967     
- Misses        832     7961     +7129     
+ Partials      300      130      -170     
```

| [Files with missing lines](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) | Coverage Δ | |
|---|---|---|
| [scrapy/core/engine.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fcore%2Fengine.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvZW5naW5lLnB5) | `17.04% <0.00%> (-72.10%)` | :arrow_down: |
| [scrapy/core/scraper.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fcore%2Fscraper.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvc2NyYXBlci5weQ==) | `24.21% <0.00%> (-62.91%)` | :arrow_down: |
| [scrapy/commands/parse.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fcommands%2Fparse.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvbW1hbmRzL3BhcnNlLnB5) | `23.76% <33.33%> (-64.63%)` | :arrow_down: |
| [scrapy/exporters.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Fexporters.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2V4cG9ydGVycy5weQ==) | `27.46% <0.00%> (-72.54%)` | :arrow_down: |
| [scrapy/utils/serialize.py](https://app.codecov.io/gh/scrapy/scrapy/pull/6719?src=pr&el=tree&filepath=scrapy%2Futils%2Fserialize.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L3V0aWxzL3NlcmlhbGl6ZS5weQ==) | `41.66% <0.00%> (-52.78%)` | :arrow_down: |

... and [141 files with indirect coverage changes](https://app.codecov.io/gh/scrapy/scrapy/pull/6719/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy)

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index a064bf5b2..312af3b2e 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -1,5 +1,10 @@
 name: Checks
-on: [push, pull_request]
+on:
+  push:
+    branches:
+    - master
+    - '[0-9]+.[0-9]+'
+  pull_request:
 
 concurrency:
   group: ${{github.workflow}}-${{ github.ref }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8e01ffd88..d1589f4f7 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -22,7 +22,7 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: "3.13"
-      - run: | 
+      - run: |
           python -m pip install --upgrade build
           python -m build
       - name: Publish to PyPI
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 18402b908..0d1a76247 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,3 +11,7 @@ repos:
   - id: blacken-docs
     additional_dependencies:
     - black==24.10.0
+- repo: https://github.com/pre-commit/pre-commit-hooks
+  rev: v5.0.0
+  hooks:
+  - id: trailing-whitespace
diff --git a/README.rst b/README.rst
index 3f468953e..cf7c6043c 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,6 @@
 .. image:: https://scrapy.org/img/scrapylogo.png
    :target: https://scrapy.org/
-   
+
 ======
 Scrapy
 ======
diff --git a/docs/faq.rst b/docs/faq.rst
index f81ec3601..da255f29e 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -361,16 +361,18 @@ method for this purpose. For example:
 
     from copy import deepcopy
 
-    from itemadapter import is_item, ItemAdapter
+    from itemadapter import ItemAdapter
+    from scrapy import Request
 
 
     class MultiplyItemsMiddleware:
         def process_spider_output(self, response, result, spider):
-            for item in result:
-                if is_item(item):
-                    adapter = ItemAdapter(item)
-                    for _ in range(adapter["multiply_by"]):
-                        yield deepcopy(item)
+            for item_or_request in result:
+                if isinstance(item_or_request, Request):
+                    continue
+                adapter = ItemAdapter(item)
+                for _ in range(adapter["multiply_by"]):
+                    yield deepcopy(item)
 
 Does Scrapy support IPv6 addresses?
 -----------------------------------
@@ -410,7 +412,7 @@ How can I make a blank request?
 -------------------------------
 
 .. code-block:: python
-    
+
     from scrapy import Request
 
 
diff --git a/docs/intro/install.rst b/docs/intro/install.rst
index 82a0e18c5..488a66f36 100644
--- a/docs/intro/install.rst
+++ b/docs/intro/install.rst
@@ -111,7 +111,7 @@ Once you've installed `Anaconda`_ or `Miniconda`_, install Scrapy with::
 To install Scrapy on Windows using ``pip``:
 
 .. warning::
-    This installation method requires “Microsoft Visual C++” for installing some 
+    This installation method requires “Microsoft Visual C++” for installing some
     Scrapy dependencies, which demands significantly more disk space than Anaconda.
 
 #. Download and execute `Microsoft C++ Build Tools`_ to install the Visual Studio Installer.
@@ -123,7 +123,7 @@ To install Scrapy on Windows using ``pip``:
 #. Check the installation details and make sure following packages are selected as optional components:
 
     * **MSVC**  (e.g MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.23) )
-    
+
     * **Windows SDK**  (e.g Windows 10 SDK (10.0.18362.0))
 
 #. Install the Visual Studio Build Tools.
diff --git a/docs/intro/tutorial.rst b/docs/intro/tutorial.rst
index 6e6caebf1..5041b49ea 100644
--- a/docs/intro/tutorial.rst
+++ b/docs/intro/tutorial.rst
@@ -292,7 +292,7 @@ As an alternative, you could've written:
     >>> response.css("title::text")[0].get()
     'Quotes to Scrape'
 
-Accessing an index on a :class:`~scrapy.selector.SelectorList` instance will 
+Accessing an index on a :class:`~scrapy.selector.SelectorList` instance will
 raise an :exc:`IndexError` exception if there are no results:
 
 .. code-block:: pycon
@@ -302,8 +302,8 @@ raise an :exc:`IndexError` exception if there are no results:
     ...
     IndexError: list index out of range
 
-You might want to use ``.get()`` directly on the 
-:class:`~scrapy.selector.SelectorList` instance instead, which returns ``None`` 
+You might want to use ``.get()`` directly on the
+:class:`~scrapy.selector.SelectorList` instance instead, which returns ``None``
 if there are no results:
 
 .. code-block:: pycon
diff --git a/docs/news.rst b/docs/news.rst
index 8230c3aef..9a68f8852 100644
--- a/docs/news.rst
+++ b/docs/news.rst
@@ -934,10 +934,10 @@ Modified requirements
 Backward-incompatible changes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
--   The value of the :setting:`FEED_STORE_EMPTY` setting is now ``True`` 
-    instead of ``False``. In earlier Scrapy versions empty files were created 
-    even when this setting was ``False`` (which was a bug that is now fixed), 
-    so the new default should keep the old behavior. (:issue:`872`, 
+-   The value of the :setting:`FEED_STORE_EMPTY` setting is now ``True``
+    instead of ``False``. In earlier Scrapy versions empty files were created
+    even when this setting was ``False`` (which was a bug that is now fixed),
+    so the new default should keep the old behavior. (:issue:`872`,
     :issue:`5847`)
 
 Deprecation removals
diff --git a/docs/topics/api.rst b/docs/topics/api.rst
index f7cffb61b..edc625be8 100644
--- a/docs/topics/api.rst
+++ b/docs/topics/api.rst
@@ -88,7 +88,7 @@ how you :ref:`configure the downloader middlewares
         The execution engine, which coordinates the core crawling logic
         between the scheduler, downloader and spiders.
 
-        Some extension may want to access the Scrapy engine, to inspect  or 
+        Some extension may want to access the Scrapy engine, to inspect  or
         modify the downloader and scheduler behaviour, although this is an
         advanced use and this API is not yet stable.
 
diff --git a/docs/topics/architecture.rst b/docs/topics/architecture.rst
index 0370dc538..4e53b6e3d 100644
--- a/docs/topics/architecture.rst
+++ b/docs/topics/architecture.rst
@@ -87,8 +87,8 @@ of the system, and triggering events when certain actions occur. See the
 Scheduler
 ---------
 
-The :ref:`scheduler <topics-scheduler>` receives requests from the engine and 
-enqueues them for feeding them later (also to the engine) when the engine 
+The :ref:`scheduler <topics-scheduler>` receives requests from the engine and
+enqueues them for feeding them later (also to the engine) when the engine
 requests them.
 
 .. _component-downloader:
diff --git a/docs/topics/exporters.rst b/docs/topics/exporters.rst
index 7a85c099b..5c078568b 100644
--- a/docs/topics/exporters.rst
+++ b/docs/topics/exporters.rst
@@ -224,7 +224,7 @@ BaseItemExporter
       .. [1] Not all exporters respect the specified field order.
       .. [2] When using :ref:`item objects <item-types>` that do not expose
              all their possible fields, exporters that do not support exporting
-             a different subset of fields per item will only export the fields 
+             a different subset of fields per item will only export the fields
              found in the first item exported.
 
    .. attribute:: export_empty_fields
diff --git a/docs/topics/extensions.rst b/docs/topics/extensions.rst
index c47a3226a..23bbcfcb5 100644
--- a/docs/topics/extensions.rst
+++ b/docs/topics/extensions.rst
@@ -256,14 +256,14 @@ Spider state extension
 Manages spider state data by loading it before a crawl and saving it after.
 
 Give a value to the :setting:`JOBDIR` setting to enable this extension.
-When enabled, this extension manages the :attr:`~scrapy.Spider.state` 
+When enabled, this extension manages the :attr:`~scrapy.Spider.state`
 attribute of your :class:`~scrapy.Spider` instance:
-    
--   When your spider closes (:signal:`spider_closed`), the contents of its 
-    :attr:`~scrapy.Spider.state` attribute are serialized into a file named 
+
+-   When your spider closes (:signal:`spider_closed`), the contents of its
+    :attr:`~scrapy.Spider.state` attribute are serialized into a file named
     ``spider.state`` in the :setting:`JOBDIR` folder.
--   When your spider opens (:signal:`spider_opened`), if a previously-generated 
-    ``spider.state`` file exists in the :setting:`JOBDIR` folder, it is loaded 
+-   When your spider opens (:signal:`spider_opened`), if a previously-generated
+    ``spider.state`` file exists in the :setting:`JOBDIR` folder, it is loaded
     into the :attr:`~scrapy.Spider.state` attribute.
 
 
@@ -291,8 +291,8 @@ settings:
 
 .. note::
 
-   When a certain closing condition is met, requests which are 
-   currently in the downloader queue (up to :setting:`CONCURRENT_REQUESTS` 
+   When a certain closing condition is met, requests which are
+   currently in the downloader queue (up to :setting:`CONCURRENT_REQUESTS`
    requests) are still processed.
 
 .. setting:: CLOSESPIDER_TIMEOUT
diff --git a/docs/topics/feed-exports.rst b/docs/topics/feed-exports.rst
index 07a3f3678..7f401f0c7 100644
--- a/docs/topics/feed-exports.rst
+++ b/docs/topics/feed-exports.rst
@@ -180,7 +180,7 @@ FTP supports two different connection modes: `active or passive
 mode by default. To use the active connection mode instead, set the
 :setting:`FEED_STORAGE_FTP_ACTIVE` setting to ``True``.
 
-The default value for the ``overwrite`` key in the :setting:`FEEDS` for this 
+The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
 storage backend is: ``True``.
 
 .. caution:: The value ``True`` in ``overwrite`` will cause you to lose the
@@ -222,7 +222,7 @@ feeds using these settings:
 -   :setting:`AWS_ENDPOINT_URL`
 -   :setting:`AWS_REGION_NAME`
 
-The default value for the ``overwrite`` key in the :setting:`FEEDS` for this 
+The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
 storage backend is: ``True``.
 
 .. caution:: The value ``True`` in ``overwrite`` will cause you to lose the
@@ -255,7 +255,7 @@ You can set a *Project ID* and *Access Control List (ACL)* through the following
 -   :setting:`FEED_STORAGE_GCS_ACL`
 -   :setting:`GCS_PROJECT_ID`
 
-The default value for the ``overwrite`` key in the :setting:`FEEDS` for this 
+The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
 storage backend is: ``True``.
 
 .. caution:: The value ``True`` in ``overwrite`` will cause you to lose the
@@ -587,8 +587,8 @@ FEED_STORE_EMPTY
 Default: ``True``
 
 Whether to export empty feeds (i.e. feeds with no items).
-If ``False``, and there are no items to export, no new files are created and 
-existing files are not modified, even if the :ref:`overwrite feed option 
+If ``False``, and there are no items to export, no new files are created and
+existing files are not modified, even if the :ref:`overwrite feed option
 <feed-options>` is enabled.
 
 .. setting:: FEED_STORAGES
diff --git a/docs/topics/items.rst b/docs/topics/items.rst
index 7cc476863..0365c95b3 100644
--- a/docs/topics/items.rst
+++ b/docs/topics/items.rst
@@ -384,9 +384,8 @@ Supporting All Item Types
 In code that receives an item, such as methods of :ref:`item pipelines
 <topics-item-pipeline>` or :ref:`spider middlewares
 <topics-spider-middleware>`, it is a good practice to use the
-:class:`~itemadapter.ItemAdapter` class and the
-:func:`~itemadapter.is_item` function to write code that works for
-any supported item type.
+:class:`~itemadapter.ItemAdapter` class to write code that works for any
+supported item type.
 
 Other classes related to items
 ==============================
diff --git a/docs/topics/logging.rst b/docs/topics/logging.rst
index fe1c4d162..a398d6c83 100644
--- a/docs/topics/logging.rst
+++ b/docs/topics/logging.rst
@@ -266,9 +266,9 @@ e.g. in the spider's ``__init__`` method:
 If you run this spider again then INFO messages from
 ``scrapy.spidermiddlewares.httperror`` logger will be gone.
 
-You can also filter log records by :class:`~logging.LogRecord` data. For 
+You can also filter log records by :class:`~logging.LogRecord` data. For
 example, you can filter log records by message content using a substring or
-a regular expression. Create a :class:`logging.Filter` subclass 
+a regular expression. Create a :class:`logging.Filter` subclass
 and equip it with a regular expression pattern to
 filter out unwanted messages:
 
@@ -284,8 +284,8 @@ filter out unwanted messages:
             if match:
                 return False
 
-A project-level filter may be attached to the root 
-handler created by Scrapy, this is a wieldy way to 
+A project-level filter may be attached to the root
+handler created by Scrapy, this is a wieldy way to
 filter all loggers in different parts of the project
 (middlewares, spider, etc.):
 
@@ -301,7 +301,7 @@ filter all loggers in different parts of the project
          for handler in logging.root.handlers:
              handler.addFilter(ContentFilter())
 
-Alternatively, you may choose a specific logger 
+Alternatively, you may choose a specific logger
 and hide it without affecting other loggers:
 
 .. code-block:: python
diff --git a/docs/topics/media-pipeline.rst b/docs/topics/media-pipeline.rst
index f086a943e..cc1fe8703 100644
--- a/docs/topics/media-pipeline.rst
+++ b/docs/topics/media-pipeline.rst
@@ -414,7 +414,7 @@ class name. E.g. given pipeline class called MyPipeline you can set setting key:
 
 and pipeline class MyPipeline will have expiration time set to 180.
 
-The last modified time from the file is used to determine the age of the file in days, 
+The last modified time from the file is used to determine the age of the file in days,
 which is then compared to the set expiration time to determine if the file is expired.
 
 .. _topics-images-thumbnails:
@@ -519,7 +519,7 @@ See here the methods that you can override in your custom Files Pipeline:
 
       In addition to ``response``, this method receives the original
       :class:`request <scrapy.Request>`,
-      :class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and 
+      :class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and
       :class:`item <scrapy.Item>`
 
       You can override this method to customize the download path of each file.
@@ -541,9 +541,9 @@ See here the methods that you can override in your custom Files Pipeline:
             def file_path(self, request, response=None, info=None, *, item=None):
                 return "files/" + PurePosixPath(urlparse_cached(request).path).name
 
-      Similarly, you can use the ``item`` to determine the file path based on some item 
+      Similarly, you can use the ``item`` to determine the file path based on some item
       property.
-      
+
       By default the :meth:`file_path` method returns
       ``full/<request URL hash>.<extension>``.
 
@@ -677,7 +677,7 @@ See here the methods that you can override in your custom Images Pipeline:
 
       In addition to ``response``, this method receives the original
       :class:`request <scrapy.Request>`,
-      :class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and 
+      :class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and
       :class:`item <scrapy.Item>`
 
       You can override this method to customize the download path of each file.
@@ -699,9 +699,9 @@ See here the methods that you can override in your custom Images Pipeline:
             def file_path(self, request, response=None, info=None, *, item=None):
                 return "files/" + PurePosixPath(urlparse_cached(request).path).name
 
-      Similarly, you can use the ``item`` to determine the file path based on some item 
+      Similarly, you can use the ``item`` to determine the file path based on some item
       property.
-      
+
       By default the :meth:`file_path` method returns
       ``full/<request URL hash>.<extension>``.
 
diff --git a/docs/topics/practices.rst b/docs/topics/practices.rst
index 5f6798601..db91cd073 100644
--- a/docs/topics/practices.rst
+++ b/docs/topics/practices.rst
@@ -309,7 +309,7 @@ Here are some tips to keep in mind when dealing with these kinds of sites:
   services like `ProxyMesh`_. An open source alternative is `scrapoxy`_, a
   super proxy that you can attach your own proxies to.
 * use a ban avoidance service, such as `Zyte API`_, which provides a `Scrapy
-  plugin <https://github.com/scrapy-plugins/scrapy-zyte-api>`__ and additional 
+  plugin <https://github.com/scrapy-plugins/scrapy-zyte-api>`__ and additional
   features, like `AI web scraping <https://www.zyte.com/ai-web-scraping/>`__
 
 If you are still unable to prevent your bot getting banned, consider contacting
diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst
index b187f3aaf..0375e0ff1 100644
--- a/docs/topics/request-response.rst
+++ b/docs/topics/request-response.rst
@@ -31,23 +31,12 @@ Request objects
         If the URL is invalid, a :exc:`ValueError` exception is raised.
     :type url: str
 
-    :param callback: the function that will be called with the response of this
-       request (once it's downloaded) as its first parameter.
+    :param callback: sets :attr:`callback`, defaults to ``None``.
 
-       In addition to a function, the following values are supported:
-
-       -   ``None`` (default), which indicates that the spider's
-           :meth:`~scrapy.Spider.parse` method must be used.
-
-       -   :func:`~scrapy.http.request.NO_CALLBACK`
-
-       For more information, see
-       :ref:`topics-request-response-ref-request-callback-arguments`.
-
-       .. note:: If exceptions are raised during processing, ``errback`` is
-                 called instead.
-
-    :type callback: collections.abc.Callable
+        .. versionchanged:: 2.0
+            The *callback* parameter is no longer required when the *errback*
+            parameter is specified.
+    :type callback: Callable[Concatenate[Response, ...], Any] | None
 
     :param method: the HTTP method of this request. Defaults to ``'GET'``.
     :type method: str
@@ -144,23 +133,15 @@ Request objects
        Negative values are allowed in order to indicate relatively low-priority.
     :type priority: int
 
-    :param dont_filter: indicates that this request should not be filtered by
-       the scheduler or some middlewares. This is used when you want to perform
-       an identical request multiple times, to ignore the duplicates filter.
-       Use it with care, or you will get into crawling loops. Default to ``False``.
+    :param dont_filter: sets :attr:`dont_filter`, defaults to ``False``.
     :type dont_filter: bool
 
-    :param errback: a function that will be called if any exception was
-       raised while processing the request. This includes pages that failed
-       with 404 HTTP errors and such. It receives a
-       :exc:`~twisted.python.failure.Failure` as first parameter.
-       For more information,
-       see :ref:`topics-request-response-ref-errbacks` below.
+    :param errback: sets :attr:`errback`, defaults to ``None``.
 
-       .. versionchanged:: 2.0
-          The *callback* parameter is no longer required when the *errback*
-          parameter is specified.
-    :type errback: collections.abc.Callable
+        .. versionchanged:: 2.0
+            The *callback* parameter is no longer required when the *errback*
+            parameter is specified.
+    :type errback: Callable[[Failure], Any] | None
 
     :param flags:  Flags sent to the request, can be used for logging or similar purposes.
     :type flags: list
@@ -194,6 +175,25 @@ Request objects
         This attribute is read-only. To change the body of a Request use
         :meth:`replace`.
 
+    .. autoattribute:: callback
+
+    .. autoattribute:: errback
+
+    .. attribute:: Request.cb_kwargs
+
+        A dictionary that contains arbitrary metadata for this request. Its contents
+        will be passed to the Request's callback as keyword arguments. It is empty
+        for new Requests, which means by default callbacks only get a
+        :class:`~scrapy.http.Response` object as argument.
+
+        This dict is :doc:`shallow copied <library/copy>` when the request is
+        cloned using the ``copy()`` or ``replace()`` methods, and can also be
+        accessed, in your spider, from the ``response.cb_kwargs`` attribute.
+
+        In case of a failure to process the request, this dict can be accessed as
+        ``failure.request.cb_kwargs`` in the request's errback. For more information,
+        see :ref:`errback-cb_kwargs`.
+
     .. attribute:: Request.meta
        :value: {}
 
@@ -237,20 +237,7 @@ Request objects
         Also mind that the :meth:`copy` and :meth:`replace` request methods
         :doc:`shallow-copy <library/copy>` request metadata.
 
-    .. attribute:: Request.cb_kwargs
-
-        A dictionary that contains arbitrary metadata for this request. Its contents
-        will be passed to the Request's callback as keyword arguments. It is empty
-        for new Requests, which means by default callbacks only get a
-        :class:`~scrapy.http.Response` object as argument.
-
-        This dict is :doc:`shallow copied <library/copy>` when the request is
-        cloned using the ``copy()`` or ``replace()`` methods, and can also be
-        accessed, in your spider, from the ``response.cb_kwargs`` attribute.
-
-        In case of a failure to process the request, this dict can be accessed as
-        ``failure.request.cb_kwargs`` in the request's errback. For more information,
-        see :ref:`errback-cb_kwargs`.
+    .. autoattribute:: dont_filter
 
     .. autoattribute:: Request.attributes
 
@@ -1309,7 +1296,7 @@ JsonResponse objects
 
 .. class:: JsonResponse(url[, ...])
 
-    The :class:`JsonResponse` class is a subclass of :class:`TextResponse` 
-    that is used when the response has a `JSON MIME type 
-    <https://mimesniff.spec.whatwg.org/#json-mime-type>`_ in its `Content-Type` 
+    The :class:`JsonResponse` class is a subclass of :class:`TextResponse`
+    that is used when the response has a `JSON MIME type
+    <https://mimesniff.spec.whatwg.org/#json-mime-type>`_ in its `Content-Type`
     header.
diff --git a/docs/topics/selectors.rst b/docs/topics/selectors.rst
index b95e6eab3..dbef07b73 100644
--- a/docs/topics/selectors.rst
+++ b/docs/topics/selectors.rst
@@ -559,7 +559,7 @@ For example, suppose you want to extract all ``<p>`` elements inside ``<div>``
 elements. First, you would get all ``<div>`` elements:
 
 .. code-block:: pycon
-    
+
     >>> divs = response.xpath("//div")
 
 At first, you may be tempted to use the following approach, which is wrong, as
@@ -610,7 +610,7 @@ As it turns out, Scrapy selectors allow you to chain selectors, so most of the t
 you can just select by class using CSS and then switch to XPath when needed:
 
 .. code-block:: pycon
-    
+
     >>> from scrapy import Selector
     >>> sel = Selector(
     ...     text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special date</time></div>'
@@ -1032,7 +1032,7 @@ whereas the CSS lookup is translated into XPath and thus runs more efficiently,
 so performance-wise its uses are limited to situations that are not easily
 described with CSS selectors.
 
-Parsel also simplifies adding your own XPath extensions with 
+Parsel also simplifies adding your own XPath extensions with
 :func:`~parsel.xpathfuncs.set_xpathfunc`.
 
 .. _topics-selectors-ref:
diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst
index e1b1c5ad6..0a67240d6 100644
--- a/docs/topics/spiders.rst
+++ b/docs/topics/spiders.rst
@@ -379,8 +379,8 @@ The above example can also be written as follows:
         def start_requests(self):
             yield scrapy.Request(f"http://www.example.com/categories/{self.category}")
 
-If you are :ref:`running Scrapy from a script <run-from-script>`, you can 
-specify spider arguments when calling 
+If you are :ref:`running Scrapy from a script <run-from-script>`, you can
+specify spider arguments when calling
 :class:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
 :class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
 
diff --git a/docs/topics/stats.rst b/docs/topics/stats.rst
index be8ecb7a5..9572a3785 100644
--- a/docs/topics/stats.rst
+++ b/docs/topics/stats.rst
@@ -86,7 +86,7 @@ Available Stats Collectors
 Besides the basic :class:`StatsCollector` there are other Stats Collectors
 available in Scrapy which extend the basic Stats Collector. You can select
 which Stats Collector to use through the :setting:`STATS_CLASS` setting. The
-default Stats Collector used is the :class:`MemoryStatsCollector`. 
+default Stats Collector used is the :class:`MemoryStatsCollector`.
 
 .. currentmodule:: scrapy.statscollectors
 
diff --git a/scrapy/commands/parse.py b/scrapy/commands/parse.py
index 61aea3ee4..c6ed20b3b 100644
--- a/scrapy/commands/parse.py
+++ b/scrapy/commands/parse.py
@@ -6,7 +6,7 @@ import json
 import logging
 from typing import TYPE_CHECKING, Any, TypeVar, overload
 
-from itemadapter import ItemAdapter, is_item
+from itemadapter import ItemAdapter
 from twisted.internet.defer import Deferred, maybeDeferred
 from w3lib.url import is_url
 
@@ -211,10 +211,10 @@ class Command(BaseRunSpiderCommand):
     ) -> tuple[list[Any], list[Request], argparse.Namespace, int, Spider, CallbackT]:
         items, requests = [], []
         for x in spider_output:
-            if is_item(x):
-                items.append(x)
-            elif isinstance(x, Request):
+            if isinstance(x, Request):
                 requests.append(x)
+            else:
+                items.append(x)
         return items, requests, opts, depth, spider, callback
 
     def run_callback(
diff --git a/scrapy/core/engine.py b/scrapy/core/engine.py
index 61f444e31..b7a73700b 100644
--- a/scrapy/core/engine.py
+++ b/scrapy/core/engine.py
@@ -11,7 +11,6 @@ import logging
 from time import time
 from typing import TYPE_CHECKING, Any, TypeVar, cast
 
-from itemadapter import is_item
 from twisted.internet.defer import Deferred, inlineCallbacks, succeed
 from twisted.internet.task import LoopingCall
 from twisted.python.failure import Failure
@@ -194,14 +193,8 @@ class ExecutionEngine:
             else:
                 if isinstance(request_or_item, Request):
                     self.crawl(request_or_item)
-                elif is_item(request_or_item):
-                    self.scraper.start_itemproc(request_or_item, response=None)
                 else:
-                    logger.error(
-                        f"Got {request_or_item!r} among start requests. Only "
-                        f"requests and items are supported. It will be "
-                        f"ignored."
-                    )
+                    self.scraper.start_itemproc(request_or_item, response=None)
 
         if self.spider_is_idle() and self.slot.close_if_idle:
             self._spider_idle()
diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py
index 03301717d..b664b61f6 100644
--- a/scrapy/core/scraper.py
+++ b/scrapy/core/scraper.py
@@ -8,7 +8,6 @@ from collections import deque
 from collections.abc import AsyncIterable, Iterator
 from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
 
-from itemadapter import is_item
 from twisted.internet.defer import Deferred, inlineCallbacks
 from twisted.python.failure import Failure
 
@@ -298,17 +297,10 @@ class Scraper:
         if isinstance(output, Request):
             assert self.crawler.engine is not None  # typing
             self.crawler.engine.crawl(request=output)
-        elif is_item(output):
-            return self.start_itemproc(output, response=response)
         elif output is None:
             pass
         else:
-            typename = type(output).__name__
-            logger.error(
-                "Spider must return request, item, or None, got %(typename)r in %(request)s",
-                {"request": request, "typename": typename},
-                extra={"spider": spider},
-            )
+            return self.start_itemproc(output, response=response)
         return None
 
     def start_itemproc(self, item: Any, *, response: Response | None) -> Deferred[Any]:
diff --git a/scrapy/exporters.py b/scrapy/exporters.py
index 46c6aa3fa..0a641752e 100644
--- a/scrapy/exporters.py
+++ b/scrapy/exporters.py
@@ -356,12 +356,12 @@ class PythonItemExporter(BaseItemExporter):
     def _serialize_value(self, value: Any) -> Any:
         if isinstance(value, Item):
             return self.export_item(value)
+        if isinstance(value, (str, bytes)):
+            return to_unicode(value, encoding=self.encoding)
         if is_item(value):
             return dict(self._serialize_item(value))
         if is_listlike(value):
             return [self._serialize_value(v) for v in value]
-        if isinstance(value, (str, bytes)):
-            return to_unicode(value, encoding=self.encoding)
         return value
 
     def _serialize_item(self, item: Any) -> Iterable[tuple[str | bytes, Any]]:
diff --git a/scrapy/http/request/__init__.py b/scrapy/http/request/__init__.py
index e24f6874d..6d3b7a926 100644
--- a/scrapy/http/request/__init__.py
+++ b/scrapy/http/request/__init__.py
@@ -138,11 +138,60 @@ class Request(object_ref):
             )
         if not (callable(errback) or errback is None):
             raise TypeError(f"errback must be a callable, got {type(errback).__name__}")
+
+        #: :class:`~collections.abc.Callable` to parse the
+        #: :class:`~scrapy.http.Response` to this request once received.
+        #:
+        #: The callable must expect the response as its first parameter, and
+        #: support any additional keyword arguments set through
+        #: :attr:`cb_kwargs`.
+        #:
+        #: In addition to an arbitrary callable, the following values are also
+        #: supported:
+        #:
+        #: -   ``None`` (default), which indicates that the
+        #:     :meth:`~scrapy.Spider.parse` method of the spider must be used.
+        #:
+        #: -   :func:`~scrapy.http.request.NO_CALLBACK`.
+        #:
+        #: If an unhandled exception is raised during request or response
+        #: processing, i.e. by a :ref:`spider middleware
+        #: <topics-spider-middleware>`, :ref:`downloader middleware
+        #: <topics-downloader-middleware>` or download handler
+        #: (:setting:`DOWNLOAD_HANDLERS`), :attr:`errback` is called instead.
+        #:
+        #: .. tip::
+        #:     :class:`~scrapy.spidermiddlewares.httperror.HttpErrorMiddleware`
+        #:     raises exceptions for non-2xx responses by default, sending them
+        #:     to the :attr:`errback` instead.
+        #:
+        #: .. seealso::
+        #:     :ref:`topics-request-response-ref-request-callback-arguments`
         self.callback: CallbackT | None = callback
+
+        #: :class:`~collections.abc.Callable` to handle exceptions raised
+        #: during request or response processing.
+        #:
+        #: The callable must expect a :exc:`~twisted.python.failure.Failure` as
+        #: its first parameter.
+        #:
+        #: .. seealso:: :ref:`topics-request-response-ref-errbacks`
         self.errback: Callable[[Failure], Any] | None = errback
 
         self.cookies: CookiesT = cookies or {}
         self.headers: Headers = Headers(headers or {}, encoding=encoding)
+
+        #: Whether this request may be filtered out by :ref:`components
+        #: <topics-components>` that support filtering out requests (``False``,
+        #: default), or those components should not filter out this request
+        #: (``True``).
+        #:
+        #: This attribute is commonly set to ``True`` to prevent duplicate
+        #: requests from being filtered out.
+        #:
+        #: When defining the start URLs of a spider through
+        #: :attr:`~scrapy.Spider.start_urls`, this attribute is enabled by
+        #: default. See :meth:`~scrapy.Spider.start_requests`.
         self.dont_filter: bool = dont_filter
 
         self._meta: dict[str, Any] | None = dict(meta) if meta else None
diff --git a/scrapy/templates/project/module/middlewares.py.tmpl b/scrapy/templates/project/module/middlewares.py.tmpl
index 8c9a86dce..dcb2d63de 100644
--- a/scrapy/templates/project/module/middlewares.py.tmpl
+++ b/scrapy/templates/project/module/middlewares.py.tmpl
@@ -6,7 +6,7 @@
 from scrapy import signals
 
 # useful for handling different item types with a single interface
-from itemadapter import is_item, ItemAdapter
+from itemadapter import ItemAdapter
 
 
 class ${ProjectName}SpiderMiddleware:
diff --git a/scrapy/utils/serialize.py b/scrapy/utils/serialize.py
index 308e351c6..bcfae0c00 100644
--- a/scrapy/utils/serialize.py
+++ b/scrapy/utils/serialize.py
@@ -28,12 +28,12 @@ class ScrapyJSONEncoder(json.JSONEncoder):
             return str(o)
         if isinstance(o, defer.Deferred):
             return str(o)
-        if is_item(o):
-            return ItemAdapter(o).asdict()
         if isinstance(o, Request):
             return f"<{type(o).__name__} {o.method} {o.url}>"
         if isinstance(o, Response):
             return f"<{type(o).__name__} {o.status} {o.url}>"
+        if is_item(o):
+            return ItemAdapter(o).asdict()
         return super().default(o)
 
 
diff --git a/sep/sep-004.rst b/sep/sep-004.rst
index b1cef2600..7a4ebe886 100644
--- a/sep/sep-004.rst
+++ b/sep/sep-004.rst
@@ -11,7 +11,7 @@ SEP-004: Library API
 ====================
 .. note:: the library API has been implemented, but slightly different from
           proposed in this SEP. You can run a Scrapy crawler inside a Twisted
-          reactor, but not outside it. 
+          reactor, but not outside it.
 
 Introduction
 ============
diff --git a/sep/sep-007.rst b/sep/sep-007.rst
index 0ca2036ce..73ce0d338 100644
--- a/sep/sep-007.rst
+++ b/sep/sep-007.rst
@@ -96,7 +96,7 @@ specified, else utf-8 is used) and returns a new unicode object. E.g:
 
 ``clean_spaces``
 ----------------
-   
+
 Converts multispaces into single spaces for the given string. E.g:
 
 ::
diff --git a/sep/sep-008.rst b/sep/sep-008.rst
index be5987e39..1c38b1c40 100644
--- a/sep/sep-008.rst
+++ b/sep/sep-008.rst
@@ -73,8 +73,8 @@ Alternative Public API Proposal
 - ``ItemLoader.get_stored_values()`` or ``ItemLoader.get_values()`` *(returns the ``ItemLoader values)*
 - ``ItemLoader.get_output_value()``
 
-- ``ItemLoader.get_input_processor()`` or ``ItemLoader.get_in_processor()`` *(short version)* 
-- ``ItemLoader.get_output_processor()`` or ``ItemLoader.get_out_processor()`` *(short version)* 
+- ``ItemLoader.get_input_processor()`` or ``ItemLoader.get_in_processor()`` *(short version)*
+- ``ItemLoader.get_output_processor()`` or ``ItemLoader.get_out_processor()`` *(short version)*
 
 - ``ItemLoader.context``
 
diff --git a/sep/sep-014.rst b/sep/sep-014.rst
index e03a2b0f6..0a2e6b51e 100644
--- a/sep/sep-014.rst
+++ b/sep/sep-014.rst
@@ -21,7 +21,7 @@ Current flaws and inconsistencies
 2. Link extractors are inflexible and hard to maintain, link
    processing/filtering is tightly coupled. (e.g. canonicalize)
 3. Isn't possible to crawl an url directly from command line because the Spider
-   does not know which callback use. 
+   does not know which callback use.
 
 These flaws will be corrected by the changes proposed in this SEP.
 
@@ -55,7 +55,7 @@ Request Extractors
 Request Extractors takes response object and determines which requests follow.
 
 This is an enhancement to ``LinkExtractors`` which returns urls (links),
-Request Extractors return Request objects. 
+Request Extractors return Request objects.
 
 Request Processors
 ------------------
diff --git a/sep/sep-018.rst b/sep/sep-018.rst
index 13ab501ed..e6d601fe1 100644
--- a/sep/sep-018.rst
+++ b/sep/sep-018.rst
@@ -200,7 +200,7 @@ the same spider:
            # extract item from response
            return item
 
-The Spider Middleware that implements spider code 
+The Spider Middleware that implements spider code
 =================================================
 
 There's gonna be one middleware that will take care of calling the proper
@@ -625,7 +625,7 @@ Resolved:
   not the original one (think of redirections), but it does carry the ``meta``
   of the original one. The original one may not be available anymore (in
   memory) if we're using a persistent scheduler., but in that case it would be
-  the deserialized request from the persistent scheduler queue. 
+  the deserialized request from the persistent scheduler queue.
 
    - No - this would make implementation more complex and we're not sure it's
      really needed


Test patch:
diff --git a/.github/workflows/tests-macos.yml b/.github/workflows/tests-macos.yml
index c28a99982..ce0e1a6c2 100644
--- a/.github/workflows/tests-macos.yml
+++ b/.github/workflows/tests-macos.yml
@@ -1,5 +1,10 @@
 name: macOS
-on: [push, pull_request]
+on:
+  push:
+    branches:
+    - master
+    - '[0-9]+.[0-9]+'
+  pull_request:
 
 concurrency:
   group: ${{github.workflow}}-${{ github.ref }}
diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml
index 6c7842217..f74575ee1 100644
--- a/.github/workflows/tests-ubuntu.yml
+++ b/.github/workflows/tests-ubuntu.yml
@@ -1,5 +1,10 @@
 name: Ubuntu
-on: [push, pull_request]
+on:
+  push:
+    branches:
+    - master
+    - '[0-9]+.[0-9]+'
+  pull_request:
 
 concurrency:
   group: ${{github.workflow}}-${{ github.ref }}
diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml
index 45e4ca157..21d621240 100644
--- a/.github/workflows/tests-windows.yml
+++ b/.github/workflows/tests-windows.yml
@@ -1,5 +1,10 @@
 name: Windows
-on: [push, pull_request]
+on:
+  push:
+    branches:
+    - master
+    - '[0-9]+.[0-9]+'
+  pull_request:
 
 concurrency:
   group: ${{github.workflow}}-${{ github.ref }}
diff --git a/docs/_tests/quotes.html b/docs/_tests/quotes.html
index f4002ecd1..d1cfd9020 100644
--- a/docs/_tests/quotes.html
+++ b/docs/_tests/quotes.html
@@ -16,13 +16,13 @@
             </div>
             <div class="col-md-4">
                 <p>
-                
+
                     <a href="/login">Login</a>
-                
+
                 </p>
             </div>
         </div>
-    
+
 
 <div class="row">
     <div class="col-md-8">
@@ -34,16 +34,16 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" /    >
+
             <a class="tag" href="/tag/change/page/1/">change</a>
-            
+
             <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
-            
+
             <a class="tag" href="/tag/thinking/page/1/">thinking</a>
-            
+
             <a class="tag" href="/tag/world/page/1/">world</a>
-            
+
         </div>
     </div>
 
@@ -54,12 +54,12 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="abilities,choices" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="abilities,choices" /    >
+
             <a class="tag" href="/tag/abilities/page/1/">abilities</a>
-            
+
             <a class="tag" href="/tag/choices/page/1/">choices</a>
-            
+
         </div>
     </div>
 
@@ -70,18 +70,18 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" /    >
+
             <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
-            
+
             <a class="tag" href="/tag/life/page/1/">life</a>
-            
+
             <a class="tag" href="/tag/live/page/1/">live</a>
-            
+
             <a class="tag" href="/tag/miracle/page/1/">miracle</a>
-            
+
             <a class="tag" href="/tag/miracles/page/1/">miracles</a>
-            
+
         </div>
     </div>
 
@@ -92,16 +92,16 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" /    >
+
             <a class="tag" href="/tag/aliteracy/page/1/">aliteracy</a>
-            
+
             <a class="tag" href="/tag/books/page/1/">books</a>
-            
+
             <a class="tag" href="/tag/classic/page/1/">classic</a>
-            
+
             <a class="tag" href="/tag/humor/page/1/">humor</a>
-            
+
         </div>
     </div>
 
@@ -112,12 +112,12 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" /    >
+
             <a class="tag" href="/tag/be-yourself/page/1/">be-yourself</a>
-            
+
             <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
-            
+
         </div>
     </div>
 
@@ -128,14 +128,14 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="adulthood,success,value" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="adulthood,success,value" /    >
+
             <a class="tag" href="/tag/adulthood/page/1/">adulthood</a>
-            
+
             <a class="tag" href="/tag/success/page/1/">success</a>
-            
+
             <a class="tag" href="/tag/value/page/1/">value</a>
-            
+
         </div>
     </div>
 
@@ -146,12 +146,12 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="life,love" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="life,love" /    >
+
             <a class="tag" href="/tag/life/page/1/">life</a>
-            
+
             <a class="tag" href="/tag/love/page/1/">love</a>
-            
+
         </div>
     </div>
 
@@ -162,16 +162,16 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" /    >
+
             <a class="tag" href="/tag/edison/page/1/">edison</a>
-            
+
             <a class="tag" href="/tag/failure/page/1/">failure</a>
-            
+
             <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
-            
+
             <a class="tag" href="/tag/paraphrased/page/1/">paraphrased</a>
-            
+
         </div>
     </div>
 
@@ -182,10 +182,10 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" /    >
+
             <a class="tag" href="/tag/misattributed-eleanor-roosevelt/page/1/">misattributed-eleanor-roosevelt</a>
-            
+
         </div>
     </div>
 
@@ -196,73 +196,73 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="humor,obvious,simile" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="humor,obvious,simile" /    >
+
             <a class="tag" href="/tag/humor/page/1/">humor</a>
-            
+
             <a class="tag" href="/tag/obvious/page/1/">obvious</a>
-            
+
             <a class="tag" href="/tag/simile/page/1/">simile</a>
-            
+
         </div>
     </div>
 
     <nav>
         <ul class="pager">
-            
-            
+
+
             <li class="next">
                 <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
             </li>
-            
+
         </ul>
     </nav>
     </div>
     <div class="col-md-4 tags-box">
-        
+
             <h2>Top Ten tags</h2>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 28px" href="/tag/love/">love</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 26px" href="/tag/inspirational/">inspirational</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 26px" href="/tag/life/">life</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 24px" href="/tag/humor/">humor</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 22px" href="/tag/books/">books</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 14px" href="/tag/reading/">reading</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 10px" href="/tag/friendship/">friendship</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 8px" href="/tag/friends/">friends</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 8px" href="/tag/truth/">truth</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 6px" href="/tag/simile/">simile</a>
             </span>
-            
-        
+
+
     </div>
 </div>
 
diff --git a/docs/_tests/quotes1.html b/docs/_tests/quotes1.html
index f4002ecd1..d1cfd9020 100644
--- a/docs/_tests/quotes1.html
+++ b/docs/_tests/quotes1.html
@@ -16,13 +16,13 @@
             </div>
             <div class="col-md-4">
                 <p>
-                
+
                     <a href="/login">Login</a>
-                
+
                 </p>
             </div>
         </div>
-    
+
 
 <div class="row">
     <div class="col-md-8">
@@ -34,16 +34,16 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" /    >
+
             <a class="tag" href="/tag/change/page/1/">change</a>
-            
+
             <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
-            
+
             <a class="tag" href="/tag/thinking/page/1/">thinking</a>
-            
+
             <a class="tag" href="/tag/world/page/1/">world</a>
-            
+
         </div>
     </div>
 
@@ -54,12 +54,12 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="abilities,choices" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="abilities,choices" /    >
+
             <a class="tag" href="/tag/abilities/page/1/">abilities</a>
-            
+
             <a class="tag" href="/tag/choices/page/1/">choices</a>
-            
+
         </div>
     </div>
 
@@ -70,18 +70,18 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" /    >
+
             <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
-            
+
             <a class="tag" href="/tag/life/page/1/">life</a>
-            
+
             <a class="tag" href="/tag/live/page/1/">live</a>
-            
+
             <a class="tag" href="/tag/miracle/page/1/">miracle</a>
-            
+
             <a class="tag" href="/tag/miracles/page/1/">miracles</a>
-            
+
         </div>
     </div>
 
@@ -92,16 +92,16 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" /    >
+
             <a class="tag" href="/tag/aliteracy/page/1/">aliteracy</a>
-            
+
             <a class="tag" href="/tag/books/page/1/">books</a>
-            
+
             <a class="tag" href="/tag/classic/page/1/">classic</a>
-            
+
             <a class="tag" href="/tag/humor/page/1/">humor</a>
-            
+
         </div>
     </div>
 
@@ -112,12 +112,12 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" /    >
+
             <a class="tag" href="/tag/be-yourself/page/1/">be-yourself</a>
-            
+
             <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
-            
+
         </div>
     </div>
 
@@ -128,14 +128,14 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="adulthood,success,value" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="adulthood,success,value" /    >
+
             <a class="tag" href="/tag/adulthood/page/1/">adulthood</a>
-            
+
             <a class="tag" href="/tag/success/page/1/">success</a>
-            
+
             <a class="tag" href="/tag/value/page/1/">value</a>
-            
+
         </div>
     </div>
 
@@ -146,12 +146,12 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="life,love" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="life,love" /    >
+
             <a class="tag" href="/tag/life/page/1/">life</a>
-            
+
             <a class="tag" href="/tag/love/page/1/">love</a>
-            
+
         </div>
     </div>
 
@@ -162,16 +162,16 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" /    >
+
             <a class="tag" href="/tag/edison/page/1/">edison</a>
-            
+
             <a class="tag" href="/tag/failure/page/1/">failure</a>
-            
+
             <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
-            
+
             <a class="tag" href="/tag/paraphrased/page/1/">paraphrased</a>
-            
+
         </div>
     </div>
 
@@ -182,10 +182,10 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" /    >
+
             <a class="tag" href="/tag/misattributed-eleanor-roosevelt/page/1/">misattributed-eleanor-roosevelt</a>
-            
+
         </div>
     </div>
 
@@ -196,73 +196,73 @@
         </span>
         <div class="tags">
             Tags:
-            <meta class="keywords" itemprop="keywords" content="humor,obvious,simile" /    > 
-            
+            <meta class="keywords" itemprop="keywords" content="humor,obvious,simile" /    >
+
             <a class="tag" href="/tag/humor/page/1/">humor</a>
-            
+
             <a class="tag" href="/tag/obvious/page/1/">obvious</a>
-            
+
             <a class="tag" href="/tag/simile/page/1/">simile</a>
-            
+
         </div>
     </div>
 
     <nav>
         <ul class="pager">
-            
-            
+
+
             <li class="next">
                 <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
             </li>
-            
+
         </ul>
     </nav>
     </div>
     <div class="col-md-4 tags-box">
-        
+
             <h2>Top Ten tags</h2>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 28px" href="/tag/love/">love</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 26px" href="/tag/inspirational/">inspirational</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 26px" href="/tag/life/">life</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 24px" href="/tag/humor/">humor</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 22px" href="/tag/books/">books</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 14px" href="/tag/reading/">reading</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 10px" href="/tag/friendship/">friendship</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 8px" href="/tag/friends/">friends</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 8px" href="/tag/truth/">truth</a>
             </span>
-            
+
             <span class="tag-item">
             <a class="tag" style="font-size: 6px" href="/tag/simile/">simile</a>
             </span>
-            
-        
+
+
     </div>
 </div>
 
diff --git a/tests/test_crawl.py b/tests/test_crawl.py
index 5766f9313..6f4045fc8 100644
--- a/tests/test_crawl.py
+++ b/tests/test_crawl.py
@@ -1,6 +1,5 @@
 import json
 import logging
-import re
 import unittest
 from ipaddress import IPv4Address
 from socket import gethostbyname
@@ -195,23 +194,16 @@ class TestCrawl(TestCase):
 
     @defer.inlineCallbacks
     def test_start_requests_unsupported_output(self):
+        """Anything that is not a request is assumed to be an item, avoiding a
+        potentially expensive call to itemadapter.is_item, and letting instead
+        things fail when ItemAdapter is actually used on the corresponding
+        non-item object."""
+
         with LogCapture("scrapy", level=logging.ERROR) as log:
             crawler = get_crawler(StartRequestsGoodAndBadOutput)
             yield crawler.crawl(mockserver=self.mockserver)
 
-        assert len(log.records) == 2
-        assert log.records[0].msg == (
-            "Got 'data:,b' among start requests. Only requests and items "
-            "are supported. It will be ignored."
-        )
-        assert re.match(
-            (
-                r"^Got <object object at 0x[0-9a-fA-F]+> among start "
-                r"requests\. Only requests and items are supported\. It "
-                r"will be ignored\.$"
-            ),
-            log.records[1].msg,
-        )
+        assert len(log.records) == 0
 
     @defer.inlineCallbacks
     def test_start_requests_laziness(self):