From ba0d8fe46a663606b2e9e8afcfc431e50d336801 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Tue, 8 Feb 2022 14:52:00 +0800 Subject: [PATCH 01/15] add basic integration for web-poet's support on additional requests --- scrapy_poet/middleware.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scrapy_poet/middleware.py b/scrapy_poet/middleware.py index 20b7169b..3ea591fd 100644 --- a/scrapy_poet/middleware.py +++ b/scrapy_poet/middleware.py @@ -15,6 +15,7 @@ from .overrides import PerDomainOverridesRegistry from .page_input_providers import HttpResponseProvider, HttpClientProvider, MetaProvider from .injection import Injector +from .backend import enable_backend, scrapy_downloader_var logger = logging.getLogger(__name__) @@ -50,11 +51,16 @@ def __init__(self, crawler: Crawler) -> None: def from_crawler(cls: Type[InjectionMiddlewareTV], crawler: Crawler) -> InjectionMiddlewareTV: o = cls(crawler) crawler.signals.connect(o.spider_closed, signal=signals.spider_closed) + crawler.signals.connect(o.spider_opened, signal=signals.spider_opened) return o def spider_closed(self, spider: Spider) -> None: self.injector.close() + def spider_opened(self, spider): + scrapy_downloader_var.set(spider.crawler.engine.download) + enable_backend() + def process_request(self, request: Request, spider: Spider) -> Optional[DummyResponse]: """This method checks if the request is really needed and if its download could be skipped by trying to infer if a ``Response`` From 81df66420ddeff9d106e89a13007b07a20fb1e0f Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Tue, 8 Feb 2022 17:22:12 +0800 Subject: [PATCH 02/15] create provider for web-poet's new HttpClient and GenericRequest --- scrapy_poet/backend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scrapy_poet/backend.py b/scrapy_poet/backend.py index 5dba5808..c72c0ec0 100644 --- a/scrapy_poet/backend.py +++ b/scrapy_poet/backend.py @@ -7,7 +7,6 @@ from scrapy_poet.utils import scrapy_response_to_http_response - logger = logging.getLogger(__name__) From 131609023eb292304a8fd7c64750e4e4ee368cfd Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Mon, 21 Feb 2022 15:56:38 +0800 Subject: [PATCH 03/15] add tests --- tests/test_providers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_providers.py b/tests/test_providers.py index c4be0546..d38f2a28 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -223,6 +223,7 @@ async def test_http_client_provider(settings): results[0].request_downloader == mock_factory.return_value + def test_meta_provider(settings): crawler = get_crawler(Spider, settings) provider = MetaProvider(crawler) From f8a7efed98f160dbbf12da576424d01b6e6fe1fa Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Fri, 11 Mar 2022 13:33:48 +0800 Subject: [PATCH 04/15] remove ContextVar approach and use Dependency Injection in Provider instead --- scrapy_poet/middleware.py | 6 ------ tests/test_backend.py | 10 ++++++++++ tests/test_providers.py | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/scrapy_poet/middleware.py b/scrapy_poet/middleware.py index 3ea591fd..20b7169b 100644 --- a/scrapy_poet/middleware.py +++ b/scrapy_poet/middleware.py @@ -15,7 +15,6 @@ from .overrides import PerDomainOverridesRegistry from .page_input_providers import HttpResponseProvider, HttpClientProvider, MetaProvider from .injection import Injector -from .backend import enable_backend, scrapy_downloader_var logger = logging.getLogger(__name__) @@ -51,16 +50,11 @@ def __init__(self, crawler: Crawler) -> None: def from_crawler(cls: Type[InjectionMiddlewareTV], crawler: Crawler) -> InjectionMiddlewareTV: o = cls(crawler) crawler.signals.connect(o.spider_closed, signal=signals.spider_closed) - crawler.signals.connect(o.spider_opened, signal=signals.spider_opened) return o def spider_closed(self, spider: Spider) -> None: self.injector.close() - def spider_opened(self, spider): - scrapy_downloader_var.set(spider.crawler.engine.download) - enable_backend() - def process_request(self, request: Request, spider: Spider) -> Optional[DummyResponse]: """This method checks if the request is really needed and if its download could be skipped by trying to infer if a ``Response`` diff --git a/tests/test_backend.py b/tests/test_backend.py index ee027560..1ea32919 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -38,6 +38,16 @@ async def test_incompatible_scrapy_request(scrapy_backend): await scrapy_backend(req) +@pytest.mark.asyncio +async def test_incompatible_scrapy_request(scrapy_backend): + """The Request must be web_poet.Request and not anything else.""" + + req = scrapy.Request("https://example.com") + + with pytest.raises(TypeError): + await scrapy_backend(req) + + @pytest.fixture def fake_http_response(): return web_poet.HttpResponse( diff --git a/tests/test_providers.py b/tests/test_providers.py index d38f2a28..c4be0546 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -223,7 +223,6 @@ async def test_http_client_provider(settings): results[0].request_downloader == mock_factory.return_value - def test_meta_provider(settings): crawler = get_crawler(Spider, settings) provider = MetaProvider(crawler) From cc97213920900247f7f3ba5e6545b3c1a0578f33 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Fri, 11 Mar 2022 15:23:06 +0800 Subject: [PATCH 05/15] update CHANGELOG to new support on additional requests --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6c97ef9..646bef0b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,13 @@ Changelog TBR --- +* Support for the new features in ``web_poet>=0.2.0`` for supporting additional + requests inside Page Objects: + + * Created new providers for ``web_poet.Meta`` and ``web_poet.HttpClient``. + * Using the said additional requests needs ``async/await`` support in + ``asyncio``. This raises the minimum scrapy requirement to ``scrapy>=2.6.0``. + * Use the new ``web_poet.HttpResponse`` which replaces ``web_poet.ResponseData``. * Support for the new features in ``web_poet>=0.2.0`` for supporting additional requests inside Page Objects: From a25b61e4306cbd3fee5f0aa72c788578d7fa10b2 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Wed, 16 Mar 2022 13:55:32 +0800 Subject: [PATCH 06/15] update callback_for() to have async support --- CHANGELOG.rst | 1 + scrapy_poet/api.py | 22 +++++++++++++--- tests/test_callback_for.py | 51 ++++++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 646bef0b..c1a34c6c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,7 @@ TBR * Created new providers for ``web_poet.Meta`` and ``web_poet.HttpClient``. * Using the said additional requests needs ``async/await`` support in ``asyncio``. This raises the minimum scrapy requirement to ``scrapy>=2.6.0``. +* add ``async`` support for ``callback_for``. 0.3.0 (2022-01-28) diff --git a/scrapy_poet/api.py b/scrapy_poet/api.py index 53454809..faff7c7e 100644 --- a/scrapy_poet/api.py +++ b/scrapy_poet/api.py @@ -30,7 +30,7 @@ def __init__(self, url: str, request=Optional[Request]): super().__init__(url=url, request=request) -def callback_for(page_cls: Type[ItemPage]) -> Callable: +def callback_for(page_cls: Type[ItemPage], is_async: bool = False) -> Callable: """Create a callback for an :class:`web_poet.pages.ItemPage` subclass. The generated callback returns the output of the @@ -67,6 +67,15 @@ def parse(self, response): parse_book = callback_for(BookPage) + The optional ``is_async`` param can also be set to ``True`` to support async + callbacks like the following, especially when Page Objects uses additional + requests needing the ``async/await`` syntax. + + .. code-block:: python + + async def parse_book(self, response: DummyResponse, page: BookPage): + yield await page.to_item() + The generated callback could be used as a spider instance method or passed as an inline/anonymous argument. Make sure to define it as a spider attribute (as shown in the example above) if you're planning to use @@ -90,5 +99,12 @@ def parse(self, response): def parse(*args, page: page_cls, **kwargs): # type: ignore yield page.to_item() # type: ignore - setattr(parse, _CALLBACK_FOR_MARKER, True) - return parse + async def async_parse(*args, page: page_cls, **kwargs): # type: ignore + yield await page.to_item() # type: ignore + + if is_async: + setattr(async_parse, _CALLBACK_FOR_MARKER, True) + return async_parse + else: + setattr(parse, _CALLBACK_FOR_MARKER, True) + return parse diff --git a/tests/test_callback_for.py b/tests/test_callback_for.py index 7a830712..fd19f020 100644 --- a/tests/test_callback_for.py +++ b/tests/test_callback_for.py @@ -14,6 +14,11 @@ class FakeItemPage(ItemPage): def to_item(self): return 'fake item page' +class FakeItemPageAsync(ItemPage): + + async def to_item(self): + return 'fake item page' + class FakeItemWebPage(ItemWebPage): @@ -28,6 +33,12 @@ class MySpider(scrapy.Spider): parse_web = callback_for(FakeItemWebPage) +class MySpiderAsync(scrapy.Spider): + + name = 'my_spider_async' + parse_item = callback_for(FakeItemPage, is_async=True) + + def test_callback_for(): """Simple test case to ensure it works as expected.""" cb = callback_for(FakeItemPage) @@ -39,6 +50,20 @@ def test_callback_for(): assert list(result) == ['fake item page'] +@pytest.mark.asyncio +async def test_callback_for_async(): + cb = callback_for(FakeItemPage, is_async=True) + assert callable(cb) + + fake_page = FakeItemPageAsync() + response = DummyResponse('http://example.com/') + result = cb(response=response, page=fake_page) + + assert await result.__anext__() == 'fake item page' + with pytest.raises(StopAsyncIteration): + assert await result.__anext__() + + def test_callback_for_instance_method(): spider = MySpider() response = DummyResponse('http://example.com/') @@ -47,12 +72,16 @@ def test_callback_for_instance_method(): assert list(result) == ['fake item page'] -def test_callback_for_inline(): - callback = callback_for(FakeItemPage) +@pytest.mark.asyncio +async def test_callback_for_instance_method_async(): + spider = MySpiderAsync() response = DummyResponse('http://example.com/') - fake_page = FakeItemPage() - result = callback(response, page=fake_page) - assert list(result) == ['fake item page'] + fake_page = FakeItemPageAsync() + result = spider.parse_item(response, page=fake_page) + + assert await result.__anext__() == 'fake item page' + with pytest.raises(StopAsyncIteration): + assert await result.__anext__() def test_default_callback(): @@ -93,6 +122,18 @@ def test_inline_callback(): assert str(exc.value) == msg +def test_inline_callback_async(): + """Sample request with inline callback using async callback_for.""" + spider = MySpiderAsync() + cb = callback_for(FakeItemPage, is_async=True) + request = scrapy.Request('http://example.com/', callback=cb) + with pytest.raises(ValueError) as exc: + request_to_dict(request, spider) + + msg = f'Function {cb} is not an instance method in: {spider}' + assert str(exc.value) == msg + + def test_invalid_subclass(): """Classes should inherit from ItemPage.""" From eb3e837240d68e2d3513cb857f5f3ff32ec8a99a Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Wed, 16 Mar 2022 16:32:11 +0800 Subject: [PATCH 07/15] add docs mentioning async support in callback_for() --- docs/intro/advanced-tutorial.rst | 1 + docs/intro/basic-tutorial.rst | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/intro/advanced-tutorial.rst b/docs/intro/advanced-tutorial.rst index 7fea1ac5..d592d19c 100644 --- a/docs/intro/advanced-tutorial.rst +++ b/docs/intro/advanced-tutorial.rst @@ -15,6 +15,7 @@ These are mainly achieved by **scrapy-poet** implementing **providers** for them * :class:`scrapy_poet.page_input_providers.HttpClientProvider` * :class:`scrapy_poet.page_input_providers.MetaProvider` +.. _`intro-additional-requests`: Additional Requests =================== diff --git a/docs/intro/basic-tutorial.rst b/docs/intro/basic-tutorial.rst index 9ee1fb08..282f4458 100644 --- a/docs/intro/basic-tutorial.rst +++ b/docs/intro/basic-tutorial.rst @@ -198,6 +198,21 @@ returning the result of the ``to_item`` method call. We could use ``response.follow_all(links, callback_for(BookPage))``, without creating an attribute, but currently it won't work with Scrapy disk queues. +.. tip:: + + :func:`~.callback_for` also supports `async generators` via the ``is_async=True`` + parameter. In this way, having ``parse_book = callback_for(BookPage, is_async=True)`` + would result in the following: + + .. code-block:: python + + async def parse_book(self, response: DummyResponse, page: BookPage): + yield await page.to_item() + + This is useful when the Page Objects uses additional requests which relies + heavily on ``async/await`` format. More info on this in this tutorial section: + :ref:`intro-additional-requests`. + Final result ============ From 7b2d4cf9b2948e7b904d2d7ee9aa07593bbab33b Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Wed, 16 Mar 2022 16:34:43 +0800 Subject: [PATCH 08/15] force callback_for() to have 'is_async' to be keyword-only param --- scrapy_poet/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy_poet/api.py b/scrapy_poet/api.py index faff7c7e..7945c15c 100644 --- a/scrapy_poet/api.py +++ b/scrapy_poet/api.py @@ -30,7 +30,7 @@ def __init__(self, url: str, request=Optional[Request]): super().__init__(url=url, request=request) -def callback_for(page_cls: Type[ItemPage], is_async: bool = False) -> Callable: +def callback_for(page_cls: Type[ItemPage], *, is_async: bool = False) -> Callable: """Create a callback for an :class:`web_poet.pages.ItemPage` subclass. The generated callback returns the output of the From 5c4326f2ae16556a11dbd16f4e4f24802d306212 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Wed, 16 Mar 2022 16:40:26 +0800 Subject: [PATCH 09/15] update async test spider to use async PO as well --- tests/test_callback_for.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_callback_for.py b/tests/test_callback_for.py index fd19f020..650df200 100644 --- a/tests/test_callback_for.py +++ b/tests/test_callback_for.py @@ -36,7 +36,7 @@ class MySpider(scrapy.Spider): class MySpiderAsync(scrapy.Spider): name = 'my_spider_async' - parse_item = callback_for(FakeItemPage, is_async=True) + parse_item = callback_for(FakeItemPageAsync, is_async=True) def test_callback_for(): From b79dfa8eb4ffff0a644d6950e06127ea580d696d Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Fri, 18 Mar 2022 16:05:00 +0800 Subject: [PATCH 10/15] remove 'is_async' param in callback_for --- docs/intro/basic-tutorial.rst | 15 ++++++++++++--- scrapy_poet/api.py | 21 ++++++++++++++++----- tests/test_callback_for.py | 6 +++--- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/docs/intro/basic-tutorial.rst b/docs/intro/basic-tutorial.rst index 282f4458..0c4691c9 100644 --- a/docs/intro/basic-tutorial.rst +++ b/docs/intro/basic-tutorial.rst @@ -200,9 +200,18 @@ returning the result of the ``to_item`` method call. We could use .. tip:: - :func:`~.callback_for` also supports `async generators` via the ``is_async=True`` - parameter. In this way, having ``parse_book = callback_for(BookPage, is_async=True)`` - would result in the following: + :func:`~.callback_for` also supports `async generators` by checking if the + ``to_item()`` method is a coroutine. So having the following: + + .. code-block:: python + + class BookPage(web_poet.ItemWebPage): + async def to_item(self): + return await do_something_async() + + callback_for(BookPage) + + would result in: .. code-block:: python diff --git a/scrapy_poet/api.py b/scrapy_poet/api.py index 7945c15c..8520ea0b 100644 --- a/scrapy_poet/api.py +++ b/scrapy_poet/api.py @@ -1,4 +1,5 @@ from typing import Callable, Optional, Type +from inspect import iscoroutinefunction from scrapy.http import Request, Response @@ -30,7 +31,7 @@ def __init__(self, url: str, request=Optional[Request]): super().__init__(url=url, request=request) -def callback_for(page_cls: Type[ItemPage], *, is_async: bool = False) -> Callable: +def callback_for(page_cls: Type[ItemPage]) -> Callable: """Create a callback for an :class:`web_poet.pages.ItemPage` subclass. The generated callback returns the output of the @@ -67,9 +68,19 @@ def parse(self, response): parse_book = callback_for(BookPage) - The optional ``is_async`` param can also be set to ``True`` to support async - callbacks like the following, especially when Page Objects uses additional - requests needing the ``async/await`` syntax. + This also produces an async generator callable if the Page Objects's + ``to_item()`` method is a coroutine which uses the ``async/await`` syntax. + So having the following: + + .. code-block:: python + + class BookPage(web_poet.ItemWebPage): + async def to_item(self): + return await do_something_async() + + callback_for(BookPage) + + would result in: .. code-block:: python @@ -102,7 +113,7 @@ def parse(*args, page: page_cls, **kwargs): # type: ignore async def async_parse(*args, page: page_cls, **kwargs): # type: ignore yield await page.to_item() # type: ignore - if is_async: + if iscoroutinefunction(page_cls.to_item): setattr(async_parse, _CALLBACK_FOR_MARKER, True) return async_parse else: diff --git a/tests/test_callback_for.py b/tests/test_callback_for.py index 650df200..3002de71 100644 --- a/tests/test_callback_for.py +++ b/tests/test_callback_for.py @@ -36,7 +36,7 @@ class MySpider(scrapy.Spider): class MySpiderAsync(scrapy.Spider): name = 'my_spider_async' - parse_item = callback_for(FakeItemPageAsync, is_async=True) + parse_item = callback_for(FakeItemPageAsync) def test_callback_for(): @@ -52,7 +52,7 @@ def test_callback_for(): @pytest.mark.asyncio async def test_callback_for_async(): - cb = callback_for(FakeItemPage, is_async=True) + cb = callback_for(FakeItemPageAsync) assert callable(cb) fake_page = FakeItemPageAsync() @@ -125,7 +125,7 @@ def test_inline_callback(): def test_inline_callback_async(): """Sample request with inline callback using async callback_for.""" spider = MySpiderAsync() - cb = callback_for(FakeItemPage, is_async=True) + cb = callback_for(FakeItemPageAsync) request = scrapy.Request('http://example.com/', callback=cb) with pytest.raises(ValueError) as exc: request_to_dict(request, spider) From a74d264c4badba8fd6dd5471a2be5854e37c8532 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Tue, 29 Mar 2022 12:49:32 +0800 Subject: [PATCH 11/15] remove duplicated test --- tests/test_backend.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 1ea32919..ee027560 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -38,16 +38,6 @@ async def test_incompatible_scrapy_request(scrapy_backend): await scrapy_backend(req) -@pytest.mark.asyncio -async def test_incompatible_scrapy_request(scrapy_backend): - """The Request must be web_poet.Request and not anything else.""" - - req = scrapy.Request("https://example.com") - - with pytest.raises(TypeError): - await scrapy_backend(req) - - @pytest.fixture def fake_http_response(): return web_poet.HttpResponse( From 3983ed6f1d8315c9f593f27bffe5f79a3dbb4cf7 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Fri, 27 May 2022 13:04:52 +0800 Subject: [PATCH 12/15] fix duplicated entry in CHANGELOG --- CHANGELOG.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c1a34c6c..1e97b227 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,13 +5,6 @@ Changelog TBR --- -* Support for the new features in ``web_poet>=0.2.0`` for supporting additional - requests inside Page Objects: - - * Created new providers for ``web_poet.Meta`` and ``web_poet.HttpClient``. - * Using the said additional requests needs ``async/await`` support in - ``asyncio``. This raises the minimum scrapy requirement to ``scrapy>=2.6.0``. - * Use the new ``web_poet.HttpResponse`` which replaces ``web_poet.ResponseData``. * Support for the new features in ``web_poet>=0.2.0`` for supporting additional requests inside Page Objects: @@ -19,6 +12,7 @@ TBR * Created new providers for ``web_poet.Meta`` and ``web_poet.HttpClient``. * Using the said additional requests needs ``async/await`` support in ``asyncio``. This raises the minimum scrapy requirement to ``scrapy>=2.6.0``. + * add ``async`` support for ``callback_for``. From d76db345627cb3c11f3afba6d50b026288e60ba2 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Fri, 27 May 2022 13:12:45 +0800 Subject: [PATCH 13/15] Remove implementation details about callback_for() in the docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrián Chaves --- docs/intro/basic-tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/intro/basic-tutorial.rst b/docs/intro/basic-tutorial.rst index 0c4691c9..2fb59f74 100644 --- a/docs/intro/basic-tutorial.rst +++ b/docs/intro/basic-tutorial.rst @@ -200,8 +200,8 @@ returning the result of the ``to_item`` method call. We could use .. tip:: - :func:`~.callback_for` also supports `async generators` by checking if the - ``to_item()`` method is a coroutine. So having the following: + :func:`~.callback_for` also supports `async generators`. So having the + following: .. code-block:: python From f1126fb5c1862718e053714ae980672d0420ce8a Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Fri, 27 May 2022 13:23:44 +0800 Subject: [PATCH 14/15] remove else block in callback_for() --- scrapy_poet/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scrapy_poet/api.py b/scrapy_poet/api.py index 8520ea0b..bb0da171 100644 --- a/scrapy_poet/api.py +++ b/scrapy_poet/api.py @@ -116,6 +116,6 @@ async def async_parse(*args, page: page_cls, **kwargs): # type: ignore if iscoroutinefunction(page_cls.to_item): setattr(async_parse, _CALLBACK_FOR_MARKER, True) return async_parse - else: - setattr(parse, _CALLBACK_FOR_MARKER, True) - return parse + + setattr(parse, _CALLBACK_FOR_MARKER, True) + return parse From c2bfe89cedcae29cc3e9c2515dcb68d8945ef182 Mon Sep 17 00:00:00 2001 From: Kevin Lloyd Bernal Date: Fri, 27 May 2022 14:06:38 +0800 Subject: [PATCH 15/15] Update docs/intro/basic-tutorial.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrián Chaves --- docs/intro/basic-tutorial.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/intro/basic-tutorial.rst b/docs/intro/basic-tutorial.rst index 2fb59f74..6881fdac 100644 --- a/docs/intro/basic-tutorial.rst +++ b/docs/intro/basic-tutorial.rst @@ -218,9 +218,9 @@ returning the result of the ``to_item`` method call. We could use async def parse_book(self, response: DummyResponse, page: BookPage): yield await page.to_item() - This is useful when the Page Objects uses additional requests which relies - heavily on ``async/await`` format. More info on this in this tutorial section: - :ref:`intro-additional-requests`. + This is useful when the Page Objects uses additional requests, which rely + heavily on ``async/await`` syntax. More info on this in this tutorial + section: :ref:`intro-additional-requests`. Final result ============