From a52506d62ff732d6c7000496757f63a309a9a6ee Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 6 Oct 2026 15:26:08 -0700 Subject: [PATCH] fix(evaluate): serialize and parse RegExp values Based on https://github.com/microsoft/playwright-python/pull/3189 by @FadeHack. Fixes: https://github.com/microsoft/playwright-python/issues/3188 --- playwright/_impl/_js_handle.py | 14 +++++++++- playwright/_impl/_str_utils.py | 12 +++++++++ tests/async/test_page_evaluate.py | 43 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index d1e8d4fd9..db617303a 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -16,15 +16,17 @@ import collections.abc import datetime import math +import re import struct import traceback from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Pattern, Union from urllib.parse import ParseResult, urlparse, urlunparse from playwright._impl._connection import Channel, ChannelOwner, from_channel from playwright._impl._errors import Error, is_target_closed_error from playwright._impl._map import Map +from playwright._impl._str_utils import escape_regex_flags, parse_regex_flags if TYPE_CHECKING: # pragma: no cover from playwright._impl._element_handle import ElementHandle @@ -182,6 +184,9 @@ def serialize_value( return {"s": value} if isinstance(value, ParseResult): return {"u": urlunparse(value)} + if isinstance(value, Pattern): + assert isinstance(value.pattern, str), "Only str patterns are supported." + return {"r": {"p": value.pattern, "f": escape_regex_flags(value)}} if value in visitor_info.visited: return dict(ref=visitor_info.visited[value]) @@ -247,6 +252,13 @@ def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: error._stack = value["e"]["s"] return error + if "r" in value: + try: + return re.compile(value["r"]["p"], parse_regex_flags(value["r"]["f"])) + except re.error: + # JavaScript-only syntax, e.g. /(?x)/, has no re equivalent. + return value + if "a" in value: a: List = [] refs[value["id"]] = a diff --git a/playwright/_impl/_str_utils.py b/playwright/_impl/_str_utils.py index 8b3e65a39..5f967b1f6 100644 --- a/playwright/_impl/_str_utils.py +++ b/playwright/_impl/_str_utils.py @@ -35,6 +35,18 @@ def escape_regex_flags(pattern: Pattern) -> str: return flags +def parse_regex_flags(flags: str) -> int: + result = 0 + if "i" in flags: + result |= re.IGNORECASE + if "s" in flags: + result |= re.DOTALL + if "m" in flags: + result |= re.MULTILINE + # The remaining JavaScript flags (d, g, u, v, y) have no re equivalent. + return result + + def escape_for_regex(text: str) -> str: return re.sub(r"[.*+?^>${}()|[\]\\]", "\\$&", text) diff --git a/tests/async/test_page_evaluate.py b/tests/async/test_page_evaluate.py index 146be6ff7..320ea0347 100644 --- a/tests/async/test_page_evaluate.py +++ b/tests/async/test_page_evaluate.py @@ -13,10 +13,13 @@ # limitations under the License. import math +import re from datetime import datetime, timedelta, timezone from typing import Optional from urllib.parse import ParseResult, urlparse +import pytest + from playwright.async_api import Error, Page @@ -358,3 +361,43 @@ async def test_evaluate_jsonvalue_url(page: Page) -> None: url = urlparse("https://example.com/") result = await page.evaluate('() => ({ someKey: new URL("https://example.com/") })') assert result == {"someKey": url} + + +async def test_evaluate_transfer_regex(page: Page) -> None: + regex = re.compile("foo.bar", re.IGNORECASE | re.MULTILINE) + assert await page.evaluate("a => a", regex) == regex + + +async def test_evaluate_pass_regex_as_regexp(page: Page) -> None: + result = await page.evaluate( + "r => [r instanceof RegExp, r.source, r.flags, r.test('A12')]", + re.compile(r"a\d+", re.IGNORECASE | re.DOTALL | re.MULTILINE), + ) + assert result == [True, "a\\d+", "ims", True] + + +async def test_evaluate_return_regex(page: Page) -> None: + result = await page.evaluate( + "() => ({ list: [/foo/s], nested: { re: /a\\d+/gi } })" + ) + assert result == { + "list": [re.compile("foo", re.DOTALL)], + "nested": {"re": re.compile(r"a\d+", re.IGNORECASE)}, + } + + +async def test_evaluate_jsonvalue_regex(page: Page) -> None: + handle = await page.evaluate_handle("() => ({ someKey: /foo/i })") + assert await handle.json_value() == {"someKey": re.compile("foo", re.IGNORECASE)} + + +async def test_evaluate_return_regex_unsupported_by_python(page: Page) -> None: + result = await page.evaluate("() => ({ a: 1, re: /(?\\d{4})/ })") + assert result == {"a": 1, "re": {"r": {"p": "(?\\d{4})", "f": ""}}} + + +async def test_evaluate_throw_for_unsupported_regex(page: Page) -> None: + with pytest.raises(AssertionError, match="Unexpected re.Pattern flag"): + await page.evaluate("a => a", re.compile("foo", re.VERBOSE)) + with pytest.raises(AssertionError, match="Only str patterns are supported"): + await page.evaluate("a => a", re.compile(b"foo"))