Repository: microsoft/playwright-python
Pull request #2698 (2024-12-27 08:05:24)

chore: relax dependency versions
Motivation: Over the last few years it came up multiple times that its more common in the Python ecosystem to not pin to a specific version of a dependency. This PR aims to relax this, since this was requested in https://github.com/microsoft/playwright-python/issues/2693, https://github.com/microsoft/playwright-python/issues/2190, https://github.com/microsoft/playwright-python/issues/2666 to change that.

- This will keep a lower/upper range so that future versions of e.g. `pyee` won't break us as long as they are using semver correctly.
- `uv` is used to `compile` the dependencies and create a lock file. So our builds are reproducible.
- Dependabot will keep bumping the prod deps in `requirements.txt` so that our CI will run the tests on them.
- This PR supersedes https://github.com/microsoft/playwright-python/issues/2666


Patch:
diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml
index 6674eaa..0076089 100644
--- a/.azure-pipelines/publish.yml
+++ b/.azure-pipelines/publish.yml
@@ -38,6 +38,7 @@ extends:
         - script: |
             python -m pip install --upgrade pip
             pip install -r local-requirements.txt
+            pip install -r requirements.txt
             pip install -e .
             for wheel in $(python setup.py --list-wheels); do
               PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 624269f..929b05b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r local-requirements.txt
+        pip install -r requirements.txt
         pip install -e .
         python -m build --wheel
         python -m playwright install --with-deps
@@ -88,6 +89,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r local-requirements.txt
+        pip install -r requirements.txt
         pip install -e .
         python -m build --wheel
         python -m playwright install --with-deps ${{ matrix.browser }}
@@ -134,6 +136,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r local-requirements.txt
+        pip install -r requirements.txt
         pip install -e .
         python -m build --wheel
         python -m playwright install ${{ matrix.browser-channel }} --with-deps
diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml
index 99ac96c..7d83136 100644
--- a/.github/workflows/publish_docker.yml
+++ b/.github/workflows/publish_docker.yml
@@ -36,5 +36,6 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r local-requirements.txt
+        pip install -r requirements.txt
         pip install -e .
     - run: ./utils/docker/publish_docker.sh stable
diff --git a/local-requirements.txt b/local-requirements.txt
index 10dbe6e..2610edb 100644
--- a/local-requirements.txt
+++ b/local-requirements.txt
@@ -3,14 +3,14 @@ black==24.8.0
 build==1.2.2.post1
 flake8==7.1.1
 flaky==3.8.1
-mypy==1.13.0
+mypy==1.14.1
 objgraph==3.6.2
-Pillow==10.4.0
+Pillow==11.1.0
 pixelmatch==0.3.0
 pre-commit==3.5.0
 pyOpenSSL==24.3.0
 pytest==8.3.4
-pytest-asyncio==0.25.0
+pytest-asyncio==0.25.1
 pytest-cov==6.0.0
 pytest-repeat==0.9.3
 pytest-timeout==2.3.1
diff --git a/meta.yaml b/meta.yaml
index f9fc9d5..f78f0e9 100644
--- a/meta.yaml
+++ b/meta.yaml
@@ -26,8 +26,9 @@ requirements:
     - setuptools_scm
   run:
     - python >=3.9
-    - greenlet ==3.1.1
-    - pyee ==12.1.1
+    # This should be the same as the dependencies in pyproject.toml
+    - greenlet>=3.1.1,<4.0.0
+    - pyee>=12,<13
 
 test: # [build_platform == target_platform]
   requires:
diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py
index fce405d..b226e24 100644
--- a/playwright/_impl/_assertions.py
+++ b/playwright/_impl/_assertions.py
@@ -874,7 +874,7 @@ def to_expected_text_values(
     ignoreCase: Optional[bool] = None,
 ) -> Sequence[ExpectedTextValue]:
     out: List[ExpectedTextValue] = []
-    assert isinstance(items, list)
+    assert isinstance(items, (list, tuple))
     for item in items:
         if isinstance(item, str):
             o = ExpectedTextValue(
diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py
index f415d59..e5a9b14 100644
--- a/playwright/_impl/_browser_context.py
+++ b/playwright/_impl/_browser_context.py
@@ -692,7 +692,10 @@ class BrowserContext(ChannelOwner):
                 asyncio.create_task(dialog.dismiss())
 
     def _on_page_error(self, error: Error, page: Optional[Page]) -> None:
-        self.emit(BrowserContext.Events.WebError, WebError(self._loop, page, error))
+        self.emit(
+            BrowserContext.Events.WebError,
+            WebError(self._loop, self._dispatcher_fiber, page, error),
+        )
         if page:
             page.emit(Page.Events.PageError, error)
 
diff --git a/playwright/_impl/_web_error.py b/playwright/_impl/_web_error.py
index eb1b519..345f95b 100644
--- a/playwright/_impl/_web_error.py
+++ b/playwright/_impl/_web_error.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 from asyncio import AbstractEventLoop
-from typing import Optional
+from typing import Any, Optional
 
 from playwright._impl._helper import Error
 from playwright._impl._page import Page
@@ -21,9 +21,14 @@ from playwright._impl._page import Page
 
 class WebError:
     def __init__(
-        self, loop: AbstractEventLoop, page: Optional[Page], error: Error
+        self,
+        loop: AbstractEventLoop,
+        dispatcher_fiber: Any,
+        page: Optional[Page],
+        error: Error,
     ) -> None:
         self._loop = loop
+        self._dispatcher_fiber = dispatcher_fiber
         self._page = page
         self._error = error
 
diff --git a/playwright/async_api/__init__.py b/playwright/async_api/__init__.py
index a64a066..be918f5 100644
--- a/playwright/async_api/__init__.py
+++ b/playwright/async_api/__init__.py
@@ -60,6 +60,7 @@ from playwright.async_api._generated import (
     Selectors,
     Touchscreen,
     Video,
+    WebError,
     WebSocket,
     WebSocketRoute,
     Worker,
@@ -190,6 +191,7 @@ __all__ = [
     "Touchscreen",
     "Video",
     "ViewportSize",
+    "WebError",
     "WebSocket",
     "WebSocketRoute",
     "Worker",
diff --git a/playwright/sync_api/__init__.py b/playwright/sync_api/__init__.py
index 80eaf71..1364339 100644
--- a/playwright/sync_api/__init__.py
+++ b/playwright/sync_api/__init__.py
@@ -60,6 +60,7 @@ from playwright.sync_api._generated import (
     Selectors,
     Touchscreen,
     Video,
+    WebError,
     WebSocket,
     WebSocketRoute,
     Worker,
@@ -190,6 +191,7 @@ __all__ = [
     "Touchscreen",
     "Video",
     "ViewportSize",
+    "WebError",
     "WebSocket",
     "WebSocketRoute",
     "Worker",
diff --git a/pyproject.toml b/pyproject.toml
index b4de553..8c66a78 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["setuptools==75.6.0", "setuptools-scm==8.1.0", "wheel==0.45.1", "auditwheel==6.1.0"]
+requires = ["setuptools==75.6.0", "setuptools-scm==8.1.0", "wheel==0.45.1", "auditwheel==6.2.0"]
 build-backend = "setuptools.build_meta"
 
 [project]
@@ -12,9 +12,12 @@ readme = "README.md"
 license = {text = "Apache-2.0"}
 dynamic = ["version"]
 requires-python = ">=3.9"
+# Please when changing dependencies run the following commands to update requirements.txt:
+# - pip install uv==0.5.4
+# - uv pip compile pyproject.toml -o requirements.txt
 dependencies = [
-    "greenlet==3.1.1",
-    "pyee==12.1.1",
+    "pyee>=12,<13",
+    "greenlet>=3.1.1,<4.0.0"
 ]
 classifiers = [
     "Topic :: Software Development :: Testing",
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..eaa7533
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+# This file was autogenerated by uv via the following command:
+#    uv pip compile pyproject.toml -o requirements.txt
+greenlet==3.1.1
+    # via playwright (pyproject.toml)
+pyee==12.1.1
+    # via playwright (pyproject.toml)
+typing-extensions==4.12.2
+    # via pyee


Test patch:
diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml
index 9d70ae3..573370f 100644
--- a/.github/workflows/test_docker.yml
+++ b/.github/workflows/test_docker.yml
@@ -36,6 +36,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r local-requirements.txt
+        pip install -r requirements.txt
         pip install -e .
     - name: Build Docker image
       run: bash utils/docker/build.sh --amd64 ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }}
@@ -45,6 +46,7 @@ jobs:
         # Fix permissions for Git inside the container
         docker exec "${CONTAINER_ID}" chown -R root:root /root/playwright
         docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt
+        docker exec "${CONTAINER_ID}" pip install -r requirements.txt
         docker exec "${CONTAINER_ID}" pip install -e .
         docker exec "${CONTAINER_ID}" python -m build --wheel
         docker exec "${CONTAINER_ID}" xvfb-run pytest -vv tests/sync/
diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py
index 88b9c1b..dc0a1e6 100644
--- a/tests/async/test_assertions.py
+++ b/tests/async/test_assertions.py
@@ -274,6 +274,10 @@ async def test_assertions_locator_to_have_text(page: Page, server: Server) -> No
     await expect(page.locator("div")).to_have_text(
         ["Text  1", re.compile(r"Text   \d+a")]
     )
+    # Should work with a tuple
+    await expect(page.locator("div")).to_have_text(
+        ("Text  1", re.compile(r"Text   \d+a"))
+    )
 
 
 @pytest.mark.parametrize(
diff --git a/tests/async/test_browsercontext_events.py b/tests/async/test_browsercontext_events.py
index a0a3b90..8ae14de 100644
--- a/tests/async/test_browsercontext_events.py
+++ b/tests/async/test_browsercontext_events.py
@@ -17,7 +17,7 @@ from typing import Optional
 
 import pytest
 
-from playwright.async_api import Page
+from playwright.async_api import BrowserContext, Page
 from tests.utils import must
 
 from ..server import Server, TestServerRequest
@@ -198,3 +198,11 @@ async def test_page_error_event_should_work(page: Page) -> None:
     page_error = await page_error_info.value
     assert page_error.page == page
     assert "boom" in page_error.error.stack
+
+
+async def test_weberror_event_should_work(context: BrowserContext, page: Page) -> None:
+    async with context.expect_event("weberror") as error_info:
+        await page.goto('data:text/html,<script>throw new Error("Test")</script>')
+    error = await error_info.value
+    assert error.page == page
+    assert error.error.message == "Test"
diff --git a/tests/sync/test_browsercontext_events.py b/tests/sync/test_browsercontext_events.py
index 315fff0..6e44b76 100644
--- a/tests/sync/test_browsercontext_events.py
+++ b/tests/sync/test_browsercontext_events.py
@@ -16,7 +16,7 @@ from typing import Optional
 
 import pytest
 
-from playwright.sync_api import Dialog, Page
+from playwright.sync_api import BrowserContext, Dialog, Page
 
 from ..server import Server, TestServerRequest
 
@@ -198,3 +198,11 @@ def test_console_event_should_work_with_context_manager(page: Page) -> None:
     message = cm_info.value
     assert message.text == "hello"
     assert message.page == page
+
+
+def test_weberror_event_should_work(context: BrowserContext, page: Page) -> None:
+    with context.expect_event("weberror") as error_info:
+        page.goto('data:text/html,<script>throw new Error("Test")</script>')
+    error = error_info.value
+    assert error.page == page
+    assert error.error.message == "Test"