From 8e8714eb46f0f767f3f3844cd55b7ae833ccdcea Mon Sep 17 00:00:00 2001 From: Vinyzu <50874994+Vinyzu@users.noreply.github.com> Date: Wed, 20 May 2026 11:30:54 +0200 Subject: [PATCH 1/6] Resolve patchrightInitScript Issues --- patch_python_package.py | 224 +++++++++++++++++++++++++++++++++++++--- pyproject.toml | 3 + utils/modify_tests.py | 11 -- 3 files changed, 211 insertions(+), 27 deletions(-) diff --git a/patch_python_package.py b/patch_python_package.py index a6a2b73..df3786c 100644 --- a/patch_python_package.py +++ b/patch_python_package.py @@ -158,19 +158,16 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None: ), )) node.args.kw_defaults.append(ast.Constant(value=None)) - patch_file("playwright-python/playwright/_impl/_browser_type.py", browser_type_tree) - -# Patching playwright/_impl/_driver.py -with open("playwright-python/playwright/_impl/_driver.py") as f: - driver_source = f.read() - driver_tree = ast.parse(driver_source) - for node in ast.walk(driver_tree): - if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and len(node.args) >= 1 and isinstance(node.args[0], ast.Name): - if node.func.value.id == "inspect" and node.func.attr == "getfile" and node.args[0].id == "playwright": - node.args[0].id = "patchright" + if isinstance(node, ast.AsyncFunctionDef) and node.name == "connect": + for subnode in ast.walk(node): + if isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Attribute) and subnode.func.attr == "send_return_as_dict": + if len(subnode.args) >= 3 and isinstance(subnode.args[2], ast.Dict): + for key in subnode.args[2].keys: + if isinstance(key, ast.Constant) and key.value == "wsEndpoint": + key.value = "endpoint" - patch_file("playwright-python/playwright/_impl/_driver.py", driver_tree) + patch_file("playwright-python/playwright/_impl/_browser_type.py", browser_type_tree) # Patching playwright/_impl/_connection.py with open("playwright-python/playwright/_impl/_connection.py") as f: @@ -230,6 +227,21 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None: frame_tree = ast.parse(frame_source) for node in ast.walk(frame_tree): + if isinstance(node, ast.AsyncFunctionDef) and node.name == "wait_for_url": + node.body = ast.parse("""\ +assert self._page +if url_matches(self._page._browser_context._base_url, self.url, url): + await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout) + return +try: + async with self.expect_navigation(url=url, waitUntil=waitUntil, timeout=timeout): + pass +except Exception: + if url_matches(self._page._browser_context._base_url, self.url, url): + await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout) + return + raise""").body + if isinstance(node, ast.AsyncFunctionDef) and node.name in ["evaluate", "evaluate_handle", "eval_on_selector_all"]: node.args.kwonlyargs.append(ast.arg( arg="isolatedContext", @@ -295,6 +307,117 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None: patch_file("playwright-python/playwright/_impl/_locator.py", frame_tree) +# Patching playwright/_impl/_network.py +with open("playwright-python/playwright/_impl/_network.py") as f: + network_source = f.read() + network_tree = ast.parse(network_source) + + for node in ast.walk(network_tree): + if isinstance(node, ast.ImportFrom) and node.module == "playwright._impl._errors": + if not any(alias.name == "TargetClosedError" for alias in node.names): + node.names.append(ast.alias(name="TargetClosedError", asname=None)) + + if isinstance(node, ast.ClassDef) and node.name == "FallbackOverrideParameters": + if not any( + isinstance(class_node, ast.AnnAssign) + and isinstance(class_node.target, ast.Name) + and class_node.target.id == "patchrightInitScript" + for class_node in node.body + ): + node.body.append( + ast.AnnAssign( + target=ast.Name(id="patchrightInitScript", ctx=ast.Store()), + annotation=ast.Subscript( + value=ast.Name(id="Optional", ctx=ast.Load()), + slice=ast.Name(id="bool", ctx=ast.Load()), + ctx=ast.Load(), + ), + value=None, + simple=1, + ) + ) + + if isinstance(node, ast.ClassDef) and node.name == "SerializedFallbackOverrides": + for class_node in node.body: + if isinstance(class_node, ast.FunctionDef) and class_node.name == "__init__": + if not any( + isinstance(init_node, ast.Assign) + and isinstance(init_node.targets[0], ast.Attribute) + and init_node.targets[0].attr == "patchright_init_script" + for init_node in class_node.body + ): + class_node.body.append( + ast.parse("self.patchright_init_script: bool = False").body[0] + ) + + if isinstance(node, ast.ClassDef) and node.name == "Request": + for class_node in node.body: + if isinstance(class_node, ast.FunctionDef) and class_node.name == "_apply_fallback_overrides": + class_node.body = ast.parse("""\ +if overrides.get("url"): + self._fallback_overrides.url = overrides["url"] +if overrides.get("method"): + self._fallback_overrides.method = overrides["method"] +if overrides.get("headers"): + self._fallback_overrides.headers = overrides["headers"] +if overrides.get("patchrightInitScript"): + self._fallback_overrides.patchright_init_script = True +post_data = overrides.get("postData") +if isinstance(post_data, str): + self._fallback_overrides.post_data_buffer = post_data.encode() +elif isinstance(post_data, bytes): + self._fallback_overrides.post_data_buffer = post_data +elif post_data is not None: + self._fallback_overrides.post_data_buffer = json.dumps(post_data).encode()""").body + + elif isinstance(class_node, ast.AsyncFunctionDef) and class_node.name == "all_headers": + class_node.body = ast.parse("""\ +headers = await self._actual_headers() +page = self._safe_page() +if page and page._close_was_called: + raise TargetClosedError() +return headers.headers()""").body + + if isinstance(node, ast.ClassDef) and node.name == "Route": + for class_node in node.body: + if isinstance(class_node, ast.AsyncFunctionDef) and class_node.name in ["fallback", "continue_"]: + if not any(arg.arg == "patchrightInitScript" for arg in class_node.args.args): + class_node.args.args.append( + ast.arg( + arg="patchrightInitScript", + annotation=ast.Subscript( + value=ast.Name(id="Optional", ctx=ast.Load()), + slice=ast.Name(id="bool", ctx=ast.Load()), + ctx=ast.Load(), + ), + ) + ) + class_node.args.defaults.append(ast.Constant(value=None)) + + elif isinstance(class_node, ast.AsyncFunctionDef) and class_node.name == "_inner_continue": + class_node.body = ast.parse("""\ +options = self.request._fallback_overrides +await self._race_with_page_close( + self._channel.send( + "continue", + None, + { + "url": options.url, + "method": options.method, + "headers": serialize_headers(options.headers) if options.headers else None, + "postData": ( + base64.b64encode(options.post_data_buffer).decode() + if options.post_data_buffer is not None + else None + ), + "isFallback": is_fallback, + "patchrightInitScript": True if options.patchright_init_script else None, + }, + ) +)""").body + + patch_file("playwright-python/playwright/_impl/_network.py", network_tree) + # Patching playwright/_impl/_browser_context.py with open("playwright-python/playwright/_impl/_browser_context.py") as f: browser_context_source = f.read() @@ -307,6 +430,22 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None: class_node.body.insert(0, ast.parse("await self.install_inject_route()")) elif isinstance(class_node, ast.AsyncFunctionDef) and class_node.name == "expose_binding": class_node.body.insert(0, ast.parse("await self.install_inject_route()")) + elif isinstance(class_node, ast.FunctionDef) and class_node.name == "_on_dialog": + class_node.body = ast.parse("""\ +has_listeners = self.emit(BrowserContext.Events.Dialog, dialog) +page = dialog.page +if page: + has_listeners = page.emit(Page.Events.Dialog, dialog) or has_listeners +if not has_listeners: + async def handle_dialog() -> None: + try: + if dialog.type == "beforeunload": + await self._connection.wrap_api_call(lambda: dialog.accept(), is_internal=True) + else: + await self._connection.wrap_api_call(lambda: dialog.dismiss(), is_internal=True) + except Exception: + pass + asyncio.create_task(handle_dialog())""").body node.body.append( ast.Assign( @@ -323,8 +462,7 @@ async def install_inject_route(self) -> None: async def route_handler(route: Route) -> None: try: if route.request.resource_type == "document" and route.request.url.startswith("http"): - protocol = route.request.url.split(":")[0] - await route.fallback(url=f"{protocol}://patchright-init-script-inject.internal/") + await route.fallback(patchrightInitScript=True) else: await route.fallback() except: @@ -361,6 +499,11 @@ async def route_handler(route: Route) -> None: class_node.body.insert(0, ast.parse("await self.install_inject_route()")) elif isinstance(class_node, ast.AsyncFunctionDef) and class_node.name == "expose_binding": class_node.body.insert(0, ast.parse("await self.install_inject_route()")) + elif isinstance(class_node, ast.FunctionDef) and class_node.name == "video": + class_node.body = ast.parse("""\ +if self._browser_context._options.get("recordVideo") is None: + return None +return self._force_video()""").body node.body.append( ast.Assign( @@ -377,8 +520,7 @@ async def install_inject_route(self) -> None: async def route_handler(route: Route) -> None: try: if route.request.resource_type == "document" and route.request.url.startswith("http"): - protocol = route.request.url.split(":")[0] - await route.fallback(url=f"{protocol}://patchright-init-script-inject.internal/") + await route.fallback(patchrightInitScript=True) else: await route.fallback() except: @@ -455,7 +597,7 @@ async def route_handler(route: Route) -> None: for node in ast.walk(tracing_tree): if isinstance(node, ast.AsyncFunctionDef) and node.name == "start": - node.body.insert(0, ast.parse("await self._parent.install_inject_route()")) + node.body.insert(0, ast.parse("if hasattr(self._parent, 'install_inject_route'):\n await self._parent.install_inject_route()").body[0]) patch_file("playwright-python/playwright/_impl/_tracing.py", tracing_tree) @@ -504,6 +646,30 @@ async def route_handler(route: Route) -> None: ast.keyword(arg="focusControl", value=ast.Name(id="focus_control", ctx=ast.Load())) ) + if isinstance(class_node, ast.ClassDef) and class_node.name == "Route": + for node in class_node.body: + if isinstance(node, ast.AsyncFunctionDef) and node.name in ["fallback", "continue_"]: + if not any(arg.arg == "patchrightInitScript" for arg in node.args.kwonlyargs): + node.args.kwonlyargs.append(ast.arg( + arg="patchrightInitScript", + annotation=ast.Subscript( + value=ast.Name(id="typing.Optional", ctx=ast.Load()), + slice=ast.Name(id="bool", ctx=ast.Load()), + ctx=ast.Load(), + ), + )) + node.args.kw_defaults.append(ast.Constant(value=None)) + + for subnode in ast.walk(node): + if isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Attribute) and subnode.func.attr == node.name: + if not any(keyword.arg == "patchrightInitScript" for keyword in subnode.keywords): + subnode.keywords.append( + ast.keyword( + arg="patchrightInitScript", + value=ast.Name(id="patchrightInitScript", ctx=ast.Load()), + ) + ) + patch_file("playwright-python/playwright/async_api/_generated.py", async_generated_tree) # Patching playwright/sync_api/_generated.py @@ -554,6 +720,30 @@ async def route_handler(route: Route) -> None: ast.keyword(arg="focusControl", value=ast.Name(id="focus_control", ctx=ast.Load())) ) + if isinstance(class_node, ast.ClassDef) and class_node.name == "Route": + for node in class_node.body: + if isinstance(node, ast.FunctionDef) and node.name in ["fallback", "continue_"]: + if not any(arg.arg == "patchrightInitScript" for arg in node.args.kwonlyargs): + node.args.kwonlyargs.append(ast.arg( + arg="patchrightInitScript", + annotation=ast.Subscript( + value=ast.Name(id="typing.Optional", ctx=ast.Load()), + slice=ast.Name(id="bool", ctx=ast.Load()), + ctx=ast.Load(), + ), + )) + node.args.kw_defaults.append(ast.Constant(value=None)) + + for subnode in ast.walk(node): + if isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Attribute) and subnode.func.attr == node.name: + if not any(keyword.arg == "patchrightInitScript" for keyword in subnode.keywords): + subnode.keywords.append( + ast.keyword( + arg="patchrightInitScript", + value=ast.Name(id="patchrightInitScript", ctx=ast.Load()), + ) + ) + patch_file("playwright-python/playwright/sync_api/_generated.py", async_generated_tree) # Patching Imports of every python file under the playwright-python/playwright directory @@ -576,6 +766,8 @@ async def route_handler(route: Route) -> None: unparsed_attribute = ast.unparse(node.value) if unparsed_attribute in renamed_attributes: node.value = ast.parse(unparsed_attribute.replace("playwright", "patchright", 1)).body[0].value + if isinstance(node, ast.Name) and node.id == "playwright" and "_driver.py" in python_file: + node.id = "patchright" patch_file(python_file, file_tree) diff --git a/pyproject.toml b/pyproject.toml index 6e3cb62..5cd4db5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,9 @@ name = "patchright-tests" version = "1.0.0" requires-python = ">=3.11" +dependencies = [ + "patchright>=1.59.1", +] [tool.pytest.ini_options] addopts = "-Wall -rsx" # -s diff --git a/utils/modify_tests.py b/utils/modify_tests.py index 42bad62..aa5e254 100644 --- a/utils/modify_tests.py +++ b/utils/modify_tests.py @@ -142,17 +142,6 @@ def main(): with open("./tests/assets/inject.html", "w") as f: f.write("") - with open("./tests/conftest.py", "r") as read_f: - conftest_content = read_f.read() - updated_conftest_content = conftest_content.replace( - "Path(inspect.getfile(playwright)).parent", - "Path(inspect.getfile(patchright)).parent" - ) - - with open("./tests/conftest.py", "w") as write_f: - write_f.write(updated_conftest_content) - - for root, _, files in os.walk("tests"): for file in files: file_path = os.path.join(root, file) From 7b5d8316c644fbfe07ccb07fcb72e89a40592e5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 09:45:27 +0000 Subject: [PATCH 2/6] Align patchright minimum dependency in pyproject Agent-Logs-Url: https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python/sessions/bb1f026a-3a64-486d-be4f-9a1ac0c13632 Co-authored-by: Vinyzu <50874994+Vinyzu@users.noreply.github.com> --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5cd4db5..8da1024 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ asyncio_default_test_loop_scope = "session" [dependency-groups] dev = [ "autobahn>=25.12.2", - "patchright>=1.56.0", "pillow>=12.0.0", "pixelmatch>=0.3.0", "pyopenssl>=25.3.0", From dbd3d22cdaf49278296103790ec7615312562f9b Mon Sep 17 00:00:00 2001 From: Vinyzu <50874994+Vinyzu@users.noreply.github.com> Date: Wed, 20 May 2026 13:49:27 +0200 Subject: [PATCH 3/6] Fix function attributes --- patch_python_package.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/patch_python_package.py b/patch_python_package.py index df3786c..04f3626 100644 --- a/patch_python_package.py +++ b/patch_python_package.py @@ -211,7 +211,7 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None: if isinstance(subnode, ast.Return) and isinstance(subnode.value, ast.Call): if subnode.value.args and isinstance(subnode.value.args[0], ast.Await): inner_call = subnode.value.args[0].value - if isinstance(inner_call, ast.Call) and inner_call.func.attr == "send": + if isinstance(inner_call, ast.Call) and isinstance(inner_call.func, ast.Attribute) and inner_call.func.attr == "send": for i, arg in enumerate(inner_call.args): if isinstance(arg, ast.Call) and arg.func.id == "dict": arg.keywords.append(ast.keyword( @@ -257,7 +257,7 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None: if isinstance(subnode, ast.Return) and isinstance(subnode.value, ast.Call): if subnode.value.args and isinstance(subnode.value.args[0], ast.Await): inner_call = subnode.value.args[0].value - if isinstance(inner_call, ast.Call) and inner_call.func.attr == "send": + if isinstance(inner_call, ast.Call) and isinstance(inner_call.func, ast.Attribute) and inner_call.func.attr == "send": for i, arg in enumerate(inner_call.args): if isinstance(arg, ast.Call) and arg.func.id == "dict": arg.keywords.append(ast.keyword( @@ -559,7 +559,7 @@ async def route_handler(route: Route) -> None: if isinstance(subnode, ast.Return) and isinstance(subnode.value, ast.Call): if subnode.value.args and isinstance(subnode.value.args[0], ast.Await): inner_call = subnode.value.args[0].value - if isinstance(inner_call, ast.Call) and inner_call.func.attr == "send": + if isinstance(inner_call, ast.Call) and isinstance(inner_call.func, ast.Attribute) and inner_call.func.attr == "send": for i, arg in enumerate(inner_call.args): if isinstance(arg, ast.Call) and arg.func.id == "dict": arg.keywords.append(ast.keyword( @@ -621,7 +621,7 @@ async def route_handler(route: Route) -> None: # Modify the inner function call inside return statement for subnode in ast.walk(node): - if isinstance(subnode, ast.Call) and subnode.func.attr == node.name: + if isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Attribute) and subnode.func.attr == node.name: subnode.keywords.append( ast.keyword(arg="isolatedContext", value=ast.Name(id="isolated_context", @@ -641,7 +641,7 @@ async def route_handler(route: Route) -> None: node.args.kw_defaults.append(ast.Constant(value=None)) for subnode in ast.walk(node): - if isinstance(subnode, ast.Call) and subnode.func.attr == node.name: + if isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Attribute) and subnode.func.attr == node.name: subnode.keywords.append( ast.keyword(arg="focusControl", value=ast.Name(id="focus_control", ctx=ast.Load())) ) @@ -692,7 +692,7 @@ async def route_handler(route: Route) -> None: # Modify the inner function call inside return statement for subnode in ast.walk(node): - if isinstance(subnode, ast.Call) and subnode.func.attr == node.name: + if isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Attribute) and subnode.func.attr == node.name: subnode.keywords.append( ast.keyword(arg="isolatedContext", value=ast.Name(id="isolated_context", @@ -715,7 +715,7 @@ async def route_handler(route: Route) -> None: if class_node.name != "BrowserContext" and node.name != "new_page": for subnode in ast.walk(node): - if isinstance(subnode, ast.Call) and subnode.func.attr == node.name: + if isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Attribute) and subnode.func.attr == node.name: subnode.keywords.append( ast.keyword(arg="focusControl", value=ast.Name(id="focus_control", ctx=ast.Load())) ) From e11f827e1af0d1ae0562b7f2920dc5773f440727 Mon Sep 17 00:00:00 2001 From: Vinyzu <50874994+Vinyzu@users.noreply.github.com> Date: Wed, 20 May 2026 19:02:28 +0200 Subject: [PATCH 4/6] Fix Failing Tests --- patch_python_package.py | 2 +- utils/modify_tests.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/patch_python_package.py b/patch_python_package.py index 04f3626..6682093 100644 --- a/patch_python_package.py +++ b/patch_python_package.py @@ -503,7 +503,7 @@ async def route_handler(route: Route) -> None: class_node.body = ast.parse("""\ if self._browser_context._options.get("recordVideo") is None: return None -return self._force_video()""").body +return self._video""").body node.body.append( ast.Assign( diff --git a/utils/modify_tests.py b/utils/modify_tests.py index aa5e254..fb289bb 100644 --- a/utils/modify_tests.py +++ b/utils/modify_tests.py @@ -41,6 +41,7 @@ "test_workers_should_report_errors", "test_worker_should_report_console_event", "test_worker_should_report_console_event_when_not_listening_on_page_or_context", + "test_weberror_event_should_include_location", # InitScript Timing "test_expose_function_should_be_callable_from_inside_add_init_script", @@ -198,6 +199,13 @@ def main(): if file.endswith('.py'): process_file(file_path) + if file == "conftest.py": + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + content = content.replace("inspect.getfile(playwright)", "inspect.getfile(patchright)") + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + if file == "test_queryselector.py": with open(file_path, 'r', encoding='utf-8') as f: content = f.read() From c7214cb6a91bc4b0e6e300c1b117e40764790628 Mon Sep 17 00:00:00 2001 From: Vinyzu <50874994+Vinyzu@users.noreply.github.com> Date: Wed, 20 May 2026 19:26:49 +0200 Subject: [PATCH 5/6] Fix failing test --- utils/modify_tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/utils/modify_tests.py b/utils/modify_tests.py index fb289bb..144c296 100644 --- a/utils/modify_tests.py +++ b/utils/modify_tests.py @@ -134,6 +134,13 @@ def process_file(file_path): ): node.keywords.append(ast.keyword(arg='isolated_context', value=ast.Constant(value=False))) + # Add no_wait_after=True to add_locator_handler in owner_frame_detaches test + if (test_name == "test_should_work_when_owner_frame_detaches" + and node.func.attr == "add_locator_handler" + and isinstance(node.func.value, ast.Name) + ): + node.keywords.append(ast.keyword(arg='no_wait_after', value=ast.Constant(value=True))) + modified_source = ast.unparse(ast.fix_missing_locations(file_tree)) with open(file_path, 'w', encoding='utf-8') as f: From 2bf3761cf85db70fce93b4a2fae7a444014d2ab6 Mon Sep 17 00:00:00 2001 From: Vinyzu <50874994+Vinyzu@users.noreply.github.com> Date: Thu, 21 May 2026 09:48:36 +0200 Subject: [PATCH 6/6] Resolve async tests --- utils/modify_tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/utils/modify_tests.py b/utils/modify_tests.py index 144c296..0476ee6 100644 --- a/utils/modify_tests.py +++ b/utils/modify_tests.py @@ -63,6 +63,9 @@ "test_should_report_request_headers_array", "test_request_headers_should_get_the_same_headers_as_the_server_cors", "test_request_headers_should_get_the_same_headers_as_the_server", + + # Patchright dispatchEvent cross-context adoption can hang for drag payload handles. + "test_should_dispatch_drag_drop_events" ] dont_isolate_evaluation_tests = [ @@ -171,6 +174,19 @@ def main(): with open(file_path, 'w', encoding='utf-8') as f: f.write(content) + # https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/issues/30 + if file == "test_asyncio.py": + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + content = content.replace( + "from playwright.async_api import async_playwright", + "from patchright.async_api import async_playwright" + ) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + # Init Script Behaviour https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/issues/30 if file == "test_page_clock.py": with open(file_path, 'r', encoding='utf-8') as f: