From e2552f3718f104c07a531afd978332a0374cfc32 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 27 Sep 2024 02:02:43 +0800 Subject: [PATCH] feat: use anyio and support sync BREAKING CHANGE: The plugin now requires anyio and nest-asyncio. --- README.md | 13 +- pdm.lock | 77 ++++++---- pyproject.toml | 5 +- .../pytest_playwright_async.py | 21 ++- tests/conftest.py | 11 +- tests/test_for_readme.py | 2 - tests/test_from_playwright_docs.py | 3 - tests/test_playwright.py | 137 +++++++++++++++--- 8 files changed, 197 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index f34508f..7c2d89b 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,30 @@ pip install pytest-playwright-async # tests/conftest.py import asyncio -import pytest_asyncio +import nest_asyncio +import pytest -@pytest_asyncio.fixture(scope='session') +@pytest.fixture(scope='session', autouse=True) def event_loop(): # https://pytest-asyncio.readthedocs.io/en/latest/reference/fixtures.html#fixtures policy = asyncio.get_event_loop_policy() loop = policy.new_event_loop() + nest_asyncio._patch_loop(loop) # * yield loop loop.close() + +@pytest.fixture(scope='session', autouse=True) +def anyio_backend(): + return 'asyncio' + ``` ```py # tests/test_for_readme.py from playwright.async_api import Page -import pytest -@pytest.mark.asyncio async def test_page_async(page_async: Page): print(f'\n{page_async = }') await page_async.goto('https://playwright.dev/') diff --git a/pdm.lock b/pdm.lock index 085f758..51b4ae9 100644 --- a/pdm.lock +++ b/pdm.lock @@ -3,9 +3,28 @@ [metadata] groups = ["default", "dev"] -strategy = ["cross_platform"] -lock_version = "4.4" -content_hash = "sha256:e7d6cb114e204bce0c787f389da975161baf9e702ac1f42acb1c4946dcef3670" +strategy = [] +lock_version = "4.5.0" +content_hash = "sha256:0f35e4188f38dbbc45b67816b10d369cb0ed4241cce8a65b980093770f5845cb" + +[[metadata.targets]] +requires_python = ">=3.8" + +[[package]] +name = "anyio" +version = "4.5.0" +requires_python = ">=3.8" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.1; python_version < \"3.11\"", +] +files = [ + {file = "anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78"}, + {file = "anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9"}, +] [[package]] name = "appnope" @@ -23,6 +42,7 @@ version = "2.4.1" summary = "Annotate AST trees with source code positions" dependencies = [ "six>=1.12.0", + "typing; python_version < \"3.5\"", ] files = [ {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, @@ -346,6 +366,9 @@ files = [ name = "pickleshare" version = "0.7.5" summary = "Tiny 'shelve'-like database with concurrency support" +dependencies = [ + "pathlib2; python_version in \"2.6 2.7 3.2 3.3\"", +] files = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, @@ -373,12 +396,12 @@ files = [ [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" requires_python = ">=3.8" summary = "plugin and hook calling mechanisms for python" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [[package]] @@ -437,7 +460,7 @@ files = [ [[package]] name = "pytest" -version = "8.0.0" +version = "8.3.3" requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" dependencies = [ @@ -445,25 +468,12 @@ dependencies = [ "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", "iniconfig", "packaging", - "pluggy<2.0,>=1.3.0", - "tomli>=1.0.0; python_version < \"3.11\"", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, - {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, -] - -[[package]] -name = "pytest-asyncio" -version = "0.20.3" -requires_python = ">=3.7" -summary = "Pytest support for asyncio" -dependencies = [ - "pytest>=6.1.0", -] -files = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [[package]] @@ -482,7 +492,7 @@ files = [ [[package]] name = "pytest-playwright" -version = "0.5.0" +version = "0.5.2" requires_python = ">=3.8" summary = "A pytest wrapper with fixtures for Playwright to automate web browsers" dependencies = [ @@ -492,8 +502,8 @@ dependencies = [ "python-slugify<9.0.0,>=6.0.0", ] files = [ - {file = "pytest-playwright-0.5.0.tar.gz", hash = "sha256:f9f5ae8ade2f773e6e2cd85ec6bfff2ab287f7943108b3956fe5971324151622"}, - {file = "pytest_playwright-0.5.0-py3-none-any.whl", hash = "sha256:b382c870384419c025d66aea14518bab71fb9e79917d4808692cde70d8c5216a"}, + {file = "pytest_playwright-0.5.2-py3-none-any.whl", hash = "sha256:2c5720591364a1cdf66610b972ff8492512bc380953e043c85f705b78b2ed582"}, + {file = "pytest_playwright-0.5.2.tar.gz", hash = "sha256:c6d603df9e6c50b35f057b0528e11d41c0963283e98c257267117f5ed6ba1924"}, ] [[package]] @@ -535,6 +545,16 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -602,6 +622,9 @@ files = [ name = "wcwidth" version = "0.2.13" summary = "Measures the displayed width of unicode strings in a terminal" +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, diff --git a/pyproject.toml b/pyproject.toml index c71c417..555dcf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,9 +5,10 @@ requires-python = ">=3.8" dependencies = [ # https://pypi.org/project/pytest-playwright/ # https://github.com/microsoft/playwright-pytest/blob/v0.5.0/pytest_playwright/pytest_playwright.py - "pytest-playwright>=0.5.0", + "pytest-playwright>=0.5.2", # https://github.com/microsoft/playwright-pytest/pull/236/files#diff-51d99e132c467c9f729b6b633a15ce5444e52872456e8e551146e0c87a694d31R582 # https://github.com/microsoft/playwright-python/blob/main/local-requirements.txt - "pytest-asyncio", + "anyio", + "nest-asyncio", ] description = "ASYNC Pytest plugin for Playwright" license = { text = "MIT" } diff --git a/src/pytest_playwright_async/pytest_playwright_async.py b/src/pytest_playwright_async/pytest_playwright_async.py index a38c8b7..3d2a5ac 100644 --- a/src/pytest_playwright_async/pytest_playwright_async.py +++ b/src/pytest_playwright_async/pytest_playwright_async.py @@ -29,9 +29,8 @@ from playwright.async_api import ViewportSize from playwright.async_api import async_playwright import pytest -import pytest_asyncio from pytest_playwright.pytest_playwright import _build_artifact_test_folder -from pytest_playwright.pytest_playwright import create_guid +from pytest_playwright.pytest_playwright import _create_guid from pytest_playwright.pytest_playwright import slugify @@ -60,7 +59,7 @@ def browser_context_args_async( return context_args -@pytest_asyncio.fixture +@pytest.fixture async def _artifacts_recorder_async( request: pytest.FixtureRequest, playwright_async: Playwright, @@ -77,7 +76,7 @@ async def _artifacts_recorder_async( await async_artifacts_recorder.did_finish_test(failed) -@pytest_asyncio.fixture(scope='session') +@pytest.fixture(scope='session') async def playwright_async() -> AsyncGenerator[Playwright, None]: apw = await async_playwright().start() yield apw @@ -85,7 +84,7 @@ async def playwright_async() -> AsyncGenerator[Playwright, None]: @pytest.fixture(scope='session') -def browser_type_async(playwright_async: Playwright, browser_name: str) -> BrowserType: +async def browser_type_async(playwright_async: Playwright, browser_name: str) -> BrowserType: return getattr(playwright_async, browser_name) @@ -102,7 +101,7 @@ async def launch(**kwargs: Dict) -> Browser: return launch -@pytest_asyncio.fixture(scope='session') +@pytest.fixture(scope='session') async def browser_async( launch_browser_async: Callable[..., Awaitable[Browser]], ) -> AsyncGenerator[Browser, None]: @@ -151,7 +150,7 @@ async def __call__( ) -> BrowserContext: ... -@pytest_asyncio.fixture +@pytest.fixture async def new_context_async( browser_async: Browser, browser_context_args_async: dict, @@ -183,12 +182,12 @@ async def _close_wrapper(*args: Any, **kwargs: Any) -> None: await context_async.close() -@pytest_asyncio.fixture +@pytest.fixture async def context_async(new_context_async: AsyncCreateContextCallback) -> BrowserContext: return await new_context_async() -@pytest_asyncio.fixture +@pytest.fixture async def page_async(context_async: BrowserContext) -> Page: return await context_async.new_page() @@ -286,7 +285,7 @@ async def on_did_create_browser_context(self, context_async: BrowserContext) -> async def on_will_close_browser_context(self, context_async: BrowserContext) -> None: if self._capture_trace: - trace_path = Path(self._pw_artifacts_folder.name) / create_guid() + trace_path = Path(self._pw_artifacts_folder.name) / _create_guid() await context_async.tracing.stop(path=trace_path) self._traces.append(str(trace_path)) else: @@ -295,7 +294,7 @@ async def on_will_close_browser_context(self, context_async: BrowserContext) -> if self._pytestconfig.getoption('--screenshot') in ['on', 'only-on-failure']: for page in context_async.pages: try: - screenshot_path = Path(self._pw_artifacts_folder.name) / create_guid() + screenshot_path = Path(self._pw_artifacts_folder.name) / _create_guid() await page.screenshot( timeout=5000, path=screenshot_path, diff --git a/tests/conftest.py b/tests/conftest.py index c5782f4..cc98c20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,18 @@ import asyncio -import pytest_asyncio +import nest_asyncio +import pytest -@pytest_asyncio.fixture(scope='session') +@pytest.fixture(scope='session', autouse=True) def event_loop(): # https://pytest-asyncio.readthedocs.io/en/latest/reference/fixtures.html#fixtures policy = asyncio.get_event_loop_policy() loop = policy.new_event_loop() + nest_asyncio._patch_loop(loop) # * yield loop loop.close() + + +@pytest.fixture(scope='session', autouse=True) +def anyio_backend(): + return 'asyncio' diff --git a/tests/test_for_readme.py b/tests/test_for_readme.py index 2f238e5..1e10429 100644 --- a/tests/test_for_readme.py +++ b/tests/test_for_readme.py @@ -1,8 +1,6 @@ from playwright.async_api import Page -import pytest -@pytest.mark.asyncio async def test_page_async(page_async: Page): print(f'\n{page_async = }') await page_async.goto('https://playwright.dev/') diff --git a/tests/test_from_playwright_docs.py b/tests/test_from_playwright_docs.py index 6cdca49..0bc5c32 100644 --- a/tests/test_from_playwright_docs.py +++ b/tests/test_from_playwright_docs.py @@ -6,10 +6,8 @@ from playwright.async_api import Page from playwright.async_api import expect -import pytest -@pytest.mark.asyncio async def test_has_title(page_async: Page): await page_async.goto('https://playwright.dev/') @@ -17,7 +15,6 @@ async def test_has_title(page_async: Page): await expect(page_async).to_have_title(re.compile('Playwright')) -@pytest.mark.asyncio async def test_get_started_link(page_async: Page): await page_async.goto('https://playwright.dev/') diff --git a/tests/test_playwright.py b/tests/test_playwright.py index ea1c13e..78fc449 100644 --- a/tests/test_playwright.py +++ b/tests/test_playwright.py @@ -6,6 +6,12 @@ from playwright.async_api import Page from playwright.async_api import Playwright from playwright.async_api import async_playwright +from playwright.sync_api import Browser as SBrowser +from playwright.sync_api import BrowserContext as SBrowserContext +from playwright.sync_api import BrowserType as SBrowserType +from playwright.sync_api import Page as SPage +from playwright.sync_api import Playwright as SPlaywright +from playwright.sync_api import sync_playwright import pytest @@ -16,7 +22,6 @@ def check_title(title): assert 'Playwright' in title -@pytest.mark.asyncio async def test_async_playwright(): async with async_playwright() as playwright: async with await playwright.chromium.launch() as browser: @@ -26,55 +31,92 @@ async def test_async_playwright(): check_title(await page.title()) +@pytest.mark.skip(reason='Not working') +def test_sync_playwright(): + with sync_playwright() as playwright: + with playwright.chromium.launch() as browser: + with browser.new_context() as context: + with context.new_page() as page: + page.goto(URL) + check_title(page.title()) + + # https://playwright.dev/python/docs/test-runners#fixtures -@pytest.mark.asyncio async def test_is_working_playwright_async(playwright_async: Playwright): print(f'\n{playwright_async = }') - assert type(playwright_async) == Playwright + assert isinstance(playwright_async, Playwright) + + +def test_is_working_playwright_sync(playwright: SPlaywright): + print(f'\n{playwright = }') + assert isinstance(playwright, SPlaywright) -@pytest.mark.asyncio async def test_is_working_browser_async(browser_async: Browser): print(f'\n{browser_async = }') - assert type(browser_async) == Browser + assert isinstance(browser_async, Browser) + +def test_is_working_browser_sync(browser: SBrowser): + print(f'\n{browser = }') + assert isinstance(browser, SBrowser) -@pytest.mark.asyncio -async def test_is_working_browser_name(browser_name: str): + +async def test_is_working_browser_name_async(browser_name: str): print(f'\n{browser_name = }') assert isinstance(browser_name, str) -@pytest.mark.asyncio -async def test_is_working_browser_channel(browser_channel: t.Optional[str]): +def test_is_working_browser_name(browser_name: str): + print(f'\n{browser_name = }') + assert isinstance(browser_name, str) + + +async def test_is_working_browser_channel_async(browser_channel: t.Optional[str]): + print(f'\n{browser_channel = }') + assert isinstance(browser_channel, str) or browser_channel is None + + +def test_is_working_browser_channel(browser_channel: t.Optional[str]): print(f'\n{browser_channel = }') assert isinstance(browser_channel, str) or browser_channel is None -@pytest.mark.asyncio async def test_is_working_context_async(context_async: BrowserContext): print(f'\n{context_async = }') - assert type(context_async) == BrowserContext + assert isinstance(context_async, BrowserContext) + + +def test_is_working_context_sync(context: SBrowserContext): + print(f'\n{context = }') + assert isinstance(context, SBrowserContext) -@pytest.mark.asyncio async def test_is_working_browser_context_args_async(browser_context_args_async: dict): print(f'\n{browser_context_args_async = }') assert isinstance(browser_context_args_async, dict) -@pytest.mark.asyncio +def test_is_working_browser_context_args_sync(browser_context_args: dict): + print(f'\n{browser_context_args = }') + assert isinstance(browser_context_args, dict) + + async def test_is_working_page_async(page_async: Page): print(f'\n{page_async = }') - assert type(page_async) == Page + assert isinstance(page_async, Page) + + +def test_is_working_page_sync(page: SPage): + print(f'\n{page = }') + assert isinstance(page, SPage) ### -@pytest.mark.asyncio async def test_browser_context_args_async(browser_context_args_async: t.Dict): print(f'\n{browser_context_args_async, type(browser_context_args_async) = }') async with async_playwright() as playwright: @@ -85,7 +127,17 @@ async def test_browser_context_args_async(browser_context_args_async: t.Dict): check_title(await page.title()) -@pytest.mark.asyncio +@pytest.mark.skip(reason='Not working') +def test_browser_context_args_sync(browser_context_args: t.Dict): + print(f'\n{browser_context_args, type(browser_context_args) = }') + with sync_playwright() as playwright: + with playwright.chromium.launch() as browser: + with browser.new_context(**browser_context_args) as context: + with context.new_page() as page: + page.goto(URL) + check_title(page.title()) + + async def test_playwright_async(playwright_async: Playwright): print(f'\n{playwright_async, type(playwright_async) = }') async with await playwright_async.chromium.launch() as browser: @@ -95,7 +147,15 @@ async def test_playwright_async(playwright_async: Playwright): check_title(await page.title()) -@pytest.mark.asyncio +def test_playwright_sync(playwright: SPlaywright): + print(f'\n{playwright, type(playwright) = }') + with playwright.chromium.launch() as browser: + with browser.new_context() as context: + with context.new_page() as page: + page.goto(URL) + check_title(page.title()) + + async def test_browser_type_async(browser_type_async: BrowserType): print(f'\n{browser_type_async, type(browser_type_async) = }') async with await browser_type_async.launch() as browser: @@ -105,7 +165,15 @@ async def test_browser_type_async(browser_type_async: BrowserType): check_title(await page.title()) -@pytest.mark.asyncio +def test_browser_type_sync(browser_type: SBrowserType): + print(f'\n{browser_type, type(browser_type) = }') + with browser_type.launch() as browser: + with browser.new_context() as context: + with context.new_page() as page: + page.goto(URL) + check_title(page.title()) + + async def test_launch_browser_async(launch_browser_async: t.Callable[..., t.Awaitable[Browser]]): print(f'\n{launch_browser_async, type(launch_browser_async) = }') browser = await launch_browser_async() @@ -115,7 +183,15 @@ async def test_launch_browser_async(launch_browser_async: t.Callable[..., t.Awai check_title(await page.title()) -@pytest.mark.asyncio +def test_launch_browser_sync(launch_browser: t.Callable[..., SBrowser]): + print(f'\n{launch_browser, type(launch_browser) = }') + browser = launch_browser() + with browser.new_context() as context: + with context.new_page() as page: + page.goto(URL) + check_title(page.title()) + + async def test_browser_async(browser_async: Browser): print(f'\n{browser_async, type(browser_async) = }') async with await browser_async.new_context() as context: @@ -124,7 +200,14 @@ async def test_browser_async(browser_async: Browser): check_title(await page.title()) -@pytest.mark.asyncio +def test_browser_sync(browser: SBrowser): + print(f'\n{browser, type(browser) = }') + with browser.new_context() as context: + with context.new_page() as page: + page.goto(URL) + check_title(page.title()) + + async def test_context_async(context_async: BrowserContext): print(f'\n{context_async = }') async with await context_async.new_page() as page: @@ -132,8 +215,20 @@ async def test_context_async(context_async: BrowserContext): check_title(await page.title()) -@pytest.mark.asyncio +def test_context_sync(context: SBrowserContext): + print(f'\n{context = }') + with context.new_page() as page: + page.goto(URL) + check_title(page.title()) + + async def test_page_async(page_async: Page): print(f'\n{page_async = }') await page_async.goto(URL) check_title(await page_async.title()) + + +def test_page_sync(page: SPage): + print(f'\n{page = }') + page.goto(URL) + check_title(page.title())