Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion playwright/_impl/_impl_to_api_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from playwright._impl._errors import Error
from playwright._impl._map import Map
from playwright._impl._signature import signature

API_ATTR = "_pw_api_instance_"
IMPL_ATTR = "_pw_impl_instance_"
Expand Down Expand Up @@ -119,7 +120,7 @@ def to_impl(

def wrap_handler(self, handler: Callable[..., Any]) -> Callable[..., None]:
def wrapper_func(*args: Any) -> Any:
parameters = inspect.signature(handler).parameters
parameters = signature(handler).parameters
has_varargs = any(
parameter.kind == inspect.Parameter.VAR_POSITIONAL
for parameter in parameters.values()
Expand Down
3 changes: 2 additions & 1 deletion playwright/_impl/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
serialize_headers,
)
from playwright._impl._screencast import Screencast
from playwright._impl._signature import signature
from playwright._impl._video import Video
from playwright._impl._waiter import Waiter
from playwright._impl._web_storage import WebStorage
Expand All @@ -125,7 +126,7 @@ def __init__(
self.times = times

def __call__(self) -> Any:
arg_count = len(inspect.signature(self._handler).parameters)
arg_count = len(signature(self._handler).parameters)
if arg_count == 0:
return self._handler()
return self._handler(self.locator)
Expand Down
32 changes: 32 additions & 0 deletions playwright/_impl/_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import inspect
import sys
from typing import Any, Callable

if sys.version_info < (3, 14):

def signature(fn: Callable[..., Any]) -> inspect.Signature:
return inspect.signature(fn)

else:
# PEP 649 made annotations lazily evaluated, and inspect.signature()
# evaluates them eagerly by default. Handlers annotated with names that
# only exist under `if TYPE_CHECKING:` would raise NameError while all we
# need is the parameter list, so ask for unresolved forward references.
from annotationlib import Format

def signature(fn: Callable[..., Any]) -> inspect.Signature:
return inspect.signature(fn, annotation_format=Format.FORWARDREF)
27 changes: 26 additions & 1 deletion tests/async/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List
import sys
from typing import TYPE_CHECKING, List

import pytest

from playwright.async_api import ConsoleMessage, Page
from tests.server import Server

if TYPE_CHECKING:

class Unresolvable:
"""Annotation-only type, deliberately undefined at runtime."""


async def test_console_should_work(page: Page, browser_name: str) -> None:
messages: List[ConsoleMessage] = []
Expand Down Expand Up @@ -153,3 +159,22 @@ async def test_console_should_not_throw_when_there_are_console_messages_in_detac
popup = await page_info.value
# 4. Connect to the popup and make sure it doesn't throw.
assert await popup.evaluate("1 + 1") == 2


@pytest.mark.skipif(
sys.version_info < (3, 14),
reason="PEP 649 lazy annotations require Python 3.14+",
)
async def test_console_should_support_annotations_with_runtime_unresolved_types(
page: Page,
) -> None:
# Since PEP 649, annotations are lazily evaluated; the signature
# inspection done for event listeners must not resolve them eagerly.
messages: List[str] = []

def on_console(message: Unresolvable) -> None:
messages.append(message.text)

page.on("console", on_console)
await page.evaluate('() => console.log("hello")')
assert messages == ["hello"]
35 changes: 35 additions & 0 deletions tests/async/test_page_add_locator_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@
# limitations under the License.

import asyncio
import sys
from typing import TYPE_CHECKING

import pytest

from playwright.async_api import Error, Locator, Page, expect
from tests.server import Server
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE

if TYPE_CHECKING:

class Unresolvable:
"""Annotation-only type, deliberately undefined at runtime."""


async def test_should_work(page: Page, server: Server) -> None:
await page.goto(server.PREFIX + "/input/handle-locator.html")
Expand Down Expand Up @@ -437,3 +444,31 @@ async def _handler(locator: Locator) -> None:
assert await page.evaluate("window.clicked") == 0
await expect(page.locator("#interstitial")).to_be_visible()
assert "Timeout 3000ms exceeded" in error.value.message


@pytest.mark.skipif(
sys.version_info < (3, 14),
reason="PEP 649 lazy annotations require Python 3.14+",
)
async def test_should_support_annotations_with_runtime_unresolved_types(
page: Page, server: Server
) -> None:
# Since PEP 649, annotations are lazily evaluated; the signature
# inspection done for locator handlers must not resolve them eagerly.
await page.goto(server.PREFIX + "/input/handle-locator.html")

original_locator = page.get_by_text("This interstitial covers the button")
called = 0

async def handler(locator: Unresolvable) -> None:
nonlocal called
called += 1
assert locator == original_locator
await page.locator("#close").click()

await page.add_locator_handler(original_locator, handler)
await page.locator("#aside").hover()
await page.evaluate('() => window.setupAnnoyingInterstitial("mouseover", 1)')
await page.locator("#target").click()
assert called == 1
assert await page.evaluate("window.clicked") == 1
27 changes: 26 additions & 1 deletion tests/sync/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List
import sys
from typing import TYPE_CHECKING, List

import pytest

from playwright.sync_api import ConsoleMessage, Page
from tests.server import Server

if TYPE_CHECKING:

class Unresolvable:
"""Annotation-only type, deliberately undefined at runtime."""


def test_console_should_work(page: Page, browser_name: str) -> None:
messages: List[ConsoleMessage] = []
Expand Down Expand Up @@ -157,3 +163,22 @@ def test_console_should_not_throw_when_there_are_console_messages_in_detached_if
)
# 4. Connect to the popup and make sure it doesn't throw.
assert popup.value.evaluate("1 + 1") == 2


@pytest.mark.skipif(
sys.version_info < (3, 14),
reason="PEP 649 lazy annotations require Python 3.14+",
)
def test_console_should_support_annotations_with_runtime_unresolved_types(
page: Page,
) -> None:
# Since PEP 649, annotations are lazily evaluated; the signature
# inspection done for event listeners must not resolve them eagerly.
messages: List[str] = []

def on_console(message: Unresolvable) -> None:
messages.append(message.text)

page.on("console", on_console)
page.evaluate('() => console.log("hello")')
assert messages == ["hello"]
35 changes: 35 additions & 0 deletions tests/sync/test_page_add_locator_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import sys
from typing import TYPE_CHECKING

import pytest

from playwright.sync_api import Error, Locator, Page, expect
from tests.server import Server
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE

if TYPE_CHECKING:

class Unresolvable:
"""Annotation-only type, deliberately undefined at runtime."""


def test_should_work(page: Page, server: Server) -> None:
page.goto(server.PREFIX + "/input/handle-locator.html")
Expand Down Expand Up @@ -433,3 +440,31 @@ def _handler(locator: Locator) -> None:
assert page.evaluate("window.clicked") == 0
expect(page.locator("#interstitial")).to_be_visible()
assert "Timeout 3000ms exceeded" in error.value.message


@pytest.mark.skipif(
sys.version_info < (3, 14),
reason="PEP 649 lazy annotations require Python 3.14+",
)
def test_should_support_annotations_with_runtime_unresolved_types(
page: Page, server: Server
) -> None:
# Since PEP 649, annotations are lazily evaluated; the signature
# inspection done for locator handlers must not resolve them eagerly.
page.goto(server.PREFIX + "/input/handle-locator.html")

original_locator = page.get_by_text("This interstitial covers the button")
called = 0

def handler(locator: Unresolvable) -> None:
nonlocal called
called += 1
assert locator == original_locator
page.locator("#close").click()

page.add_locator_handler(original_locator, handler)
page.locator("#aside").hover()
page.evaluate('() => window.setupAnnoyingInterstitial("mouseover", 1)')
page.locator("#target").click()
assert called == 1
assert page.evaluate("window.clicked") == 1