From ac297ef83f2df4a30bd0c46893a38f6164e0c035 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Mon, 22 Sep 2025 19:42:46 +0800 Subject: [PATCH] fix: The list of annotated methods (and their arguments) are now discovered dynamically so we are always compatible with the current version of playwright This was introduced after playwright released a breaking change in a patch release - they removed the `timeout` argument from the `Frame.is_hidden()` method in microsoft/playwright-python#2892 --- README.md | 6 +- .../instrumentation/playwright/__init__.py | 19 +- .../instrumentation/playwright/targets.py | 3153 +---------------- tests/test_integration.py | 1 - tests/test_targets.py | 306 -- 5 files changed, 98 insertions(+), 3387 deletions(-) delete mode 100644 tests/test_targets.py diff --git a/README.md b/README.md index 0f32e7d..20d62b7 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,9 @@ classes (sync and async): Common actions like `launch`, `new_page`, `goto`, `click`, `type`, `screenshot`, and many more are traced with relevant attributes. -See the [`targets.py`](src/opentelemetry/instrumentation/playwright/targets.py) -file for a full list of the methods that are instrumented, along with the -parameters that will be attached to the Open Telemetry span as parameters. +You can find the full list of methods that are instrumented with the +`annotated_methods()` function from the +`opentelemetry.instrumentation.playwright.targets` module. ## License diff --git a/src/opentelemetry/instrumentation/playwright/__init__.py b/src/opentelemetry/instrumentation/playwright/__init__.py index 889d0d2..b755c27 100644 --- a/src/opentelemetry/instrumentation/playwright/__init__.py +++ b/src/opentelemetry/instrumentation/playwright/__init__.py @@ -1,6 +1,6 @@ import inspect import time -from collections.abc import Callable, Collection +from collections.abc import Callable, Collection, Mapping from contextlib import ExitStack from functools import wraps from typing import Any, ParamSpec, Type, TypedDict, TypeVar, Unpack, override @@ -19,7 +19,7 @@ from playwright._impl._async_base import AsyncContextManager from playwright._impl._sync_base import SyncContextManager -from .targets import METHODS, AttrConstructor +from .targets import AttrConstructor, annotated_methods __all__ = ["PlaywrightInstrumentor"] __version__ = "0.0.0" @@ -79,9 +79,9 @@ def instrumentation_dependencies(self) -> Collection[str]: @override def _instrument(self, **kwargs: Any): - for parent, methods in METHODS.items(): - for method, attrs in methods: - self._patch(parent, method, attrs) + for parent, methods in annotated_methods().items(): + for m in methods: + self._patch(parent, m.name, m.attrs) self._patch_context_manager(SyncContextManager) self._patch_async_context_manager(AsyncContextManager) @@ -92,8 +92,9 @@ def _uninstrument(self, **kwargs: Any): # instrumentor back to a usable state. self._exit_stack.pop_all().close() - def _patch(self, parent: Type, method: str, attrs: dict[str, AttrConstructor]): - """Patches a method on a Playwright class to add OpenTelemetry instrumentation. + def _patch(self, parent: Type, method: str, attrs: Mapping[str, AttrConstructor]): + """ + Patches a method on a Playwright class to add OpenTelemetry instrumentation. This is the core instrumentation function that: 1. Gets the original method from the parent class @@ -286,7 +287,7 @@ async def aexit_wrapper(self_, exc_type, exc_val, exc_tb): def _attr_maker( - func: Callable, attributes: dict[str, AttrConstructor] + func: Callable, attributes: Mapping[str, AttrConstructor] ) -> Callable[..., Attributes]: """ Get a callable which, when called with the same arguments as the original @@ -300,7 +301,7 @@ def _attr_maker( for attr_name in attributes: assert ( attr_name in signature.parameters - ), f"Argument '{attr_name}' not found in {func.__name__}{signature}" + ), f"Argument '{attr_name}' not found in {func.__qualname__}{signature}" def maker(*args: Any, **kwargs: Any) -> Attributes: bound = signature.bind(*args, **kwargs) diff --git a/src/opentelemetry/instrumentation/playwright/targets.py b/src/opentelemetry/instrumentation/playwright/targets.py index a220202..f4132c2 100644 --- a/src/opentelemetry/instrumentation/playwright/targets.py +++ b/src/opentelemetry/instrumentation/playwright/targets.py @@ -1,4 +1,9 @@ -from collections.abc import Callable +import functools +import inspect +import typing +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path from typing import Any, Type import playwright.async_api @@ -7,3070 +12,82 @@ AttrConstructor = Callable[[Any], AttributeValue] -# Note: This part is generated by tests/test_targets.py. Do not edit it manually. -# START:METHODS -METHODS: dict[Type, tuple[tuple[str, dict[str, AttrConstructor]], ...]] = { - playwright.async_api.BrowserType: ( - ( - "connect", - { - "expose_network": str, - "slow_mo": float, - "timeout": float, - "ws_endpoint": str, - }, - ), - ( - "connect_over_cdp", - { - "endpoint_url": str, - "slow_mo": float, - "timeout": float, - }, - ), - ( - "launch", - { - "channel": str, - "chromium_sandbox": bool, - "devtools": bool, - "downloads_path": str, - "executable_path": str, - "handle_sighup": bool, - "handle_sigint": bool, - "handle_sigterm": bool, - "headless": bool, - "ignore_default_args": bool, - "slow_mo": float, - "timeout": float, - "traces_dir": str, - }, - ), - ( - "launch_persistent_context", - { - "accept_downloads": bool, - "base_url": str, - "bypass_csp": bool, - "channel": str, - "chromium_sandbox": bool, - "device_scale_factor": float, - "devtools": bool, - "downloads_path": str, - "executable_path": str, - "handle_sighup": bool, - "handle_sigint": bool, - "handle_sigterm": bool, - "has_touch": bool, - "headless": bool, - "ignore_default_args": bool, - "ignore_https_errors": bool, - "is_mobile": bool, - "java_script_enabled": bool, - "locale": str, - "no_viewport": bool, - "offline": bool, - "record_har_omit_content": bool, - "record_har_path": str, - "record_har_url_filter": str, - "record_video_dir": str, - "slow_mo": float, - "strict_selectors": bool, - "timeout": float, - "timezone_id": str, - "traces_dir": str, - "user_agent": str, - "user_data_dir": str, - }, - ), - ), - playwright.async_api.Page: ( - ( - "add_init_script", - { - "path": str, - "script": str, - }, - ), - ( - "add_locator_handler", - { - "no_wait_after": bool, - "times": int, - }, - ), - ( - "add_script_tag", - { - "content": str, - "path": str, - "type": str, - "url": str, - }, - ), - ( - "add_style_tag", - { - "content": str, - "path": str, - "url": str, - }, - ), - ("bring_to_front", {}), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "close", - { - "reason": str, - "run_before_unload": bool, - }, - ), - ("content", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "selector": str, - "strict": bool, - "timeout": float, - "type": str, - }, - ), - ( - "drag_and_drop", - { - "force": bool, - "no_wait_after": bool, - "source": str, - "strict": bool, - "target": str, - "timeout": float, - "trial": bool, - }, - ), - ("emulate_media", {}), - ( - "eval_on_selector", - { - "expression": str, - "selector": str, - "strict": bool, - }, - ), - ( - "eval_on_selector_all", - { - "expression": str, - "selector": str, - }, - ), - ( - "evaluate", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - }, - ), - ( - "expose_binding", - { - "handle": bool, - "name": str, - }, - ), - ( - "expose_function", - { - "name": str, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "focus", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "get_attribute", - { - "name": str, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "go_back", - { - "timeout": float, - }, - ), - ( - "go_forward", - { - "timeout": float, - }, - ), - ( - "goto", - { - "referer": str, - "timeout": float, - "url": str, - }, - ), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "inner_html", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "inner_text", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "input_value", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_checked", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_disabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_editable", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_enabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_hidden", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_visible", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("opener", {}), - ("pause", {}), - ( - "pdf", - { - "display_header_footer": bool, - "footer_template": str, - "format": str, - "header_template": str, - "landscape": bool, - "outline": bool, - "page_ranges": str, - "path": str, - "prefer_css_page_size": bool, - "print_background": bool, - "scale": float, - "tagged": bool, - }, - ), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "query_selector", - { - "selector": str, - "strict": bool, - }, - ), - ( - "query_selector_all", - { - "selector": str, - }, - ), - ( - "reload", - { - "timeout": float, - }, - ), - ("remove_locator_handler", {}), - ("request_gc", {}), - ( - "route", - { - "times": int, - "url": str, - }, - ), - ( - "route_from_har", - { - "har": str, - "update": bool, - "url": str, - }, - ), - ( - "route_web_socket", - { - "url": str, - }, - ), - ( - "screenshot", - { - "full_page": bool, - "mask_color": str, - "omit_background": bool, - "path": str, - "quality": int, - "style": str, - "timeout": float, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_content", - { - "html": str, - "timeout": float, - }, - ), - ("set_extra_http_headers", {}), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("set_viewport_size", {}), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "text_content", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("title", {}), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "unroute", - { - "url": str, - }, - ), - ("unroute_all", {}), - ( - "wait_for_event", - { - "event": str, - "timeout": float, - }, - ), - ( - "wait_for_function", - { - "expression": str, - "polling": float, - "timeout": float, - }, - ), - ( - "wait_for_load_state", - { - "timeout": float, - }, - ), - ( - "wait_for_selector", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "wait_for_timeout", - { - "timeout": float, - }, - ), - ( - "wait_for_url", - { - "timeout": float, - "url": str, - }, - ), - ), - playwright.async_api.Browser: ( - ( - "close", - { - "reason": str, - }, - ), - ("new_browser_cdp_session", {}), - ( - "new_context", - { - "accept_downloads": bool, - "base_url": str, - "bypass_csp": bool, - "default_browser_type": str, - "device_scale_factor": float, - "has_touch": bool, - "ignore_https_errors": bool, - "is_mobile": bool, - "java_script_enabled": bool, - "locale": str, - "no_viewport": bool, - "offline": bool, - "record_har_omit_content": bool, - "record_har_path": str, - "record_har_url_filter": str, - "record_video_dir": str, - "storage_state": str, - "strict_selectors": bool, - "timezone_id": str, - "user_agent": str, - }, - ), - ( - "new_page", - { - "accept_downloads": bool, - "base_url": str, - "bypass_csp": bool, - "default_browser_type": str, - "device_scale_factor": float, - "has_touch": bool, - "ignore_https_errors": bool, - "is_mobile": bool, - "java_script_enabled": bool, - "locale": str, - "no_viewport": bool, - "offline": bool, - "record_har_omit_content": bool, - "record_har_path": str, - "record_har_url_filter": str, - "record_video_dir": str, - "storage_state": str, - "strict_selectors": bool, - "timezone_id": str, - "user_agent": str, - }, - ), - ( - "start_tracing", - { - "path": str, - "screenshots": bool, - }, - ), - ("stop_tracing", {}), - ), - playwright.async_api.Frame: ( - ( - "add_script_tag", - { - "content": str, - "path": str, - "type": str, - "url": str, - }, - ), - ( - "add_style_tag", - { - "content": str, - "path": str, - "url": str, - }, - ), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ("content", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "selector": str, - "strict": bool, - "timeout": float, - "type": str, - }, - ), - ( - "drag_and_drop", - { - "force": bool, - "no_wait_after": bool, - "source": str, - "strict": bool, - "target": str, - "timeout": float, - "trial": bool, - }, - ), - ( - "eval_on_selector", - { - "expression": str, - "selector": str, - "strict": bool, - }, - ), - ( - "eval_on_selector_all", - { - "expression": str, - "selector": str, - }, - ), - ( - "evaluate", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "focus", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("frame_element", {}), - ( - "get_attribute", - { - "name": str, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "goto", - { - "referer": str, - "timeout": float, - "url": str, - }, - ), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "inner_html", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "inner_text", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "input_value", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_checked", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_disabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_editable", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_enabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_hidden", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_visible", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "query_selector", - { - "selector": str, - "strict": bool, - }, - ), - ( - "query_selector_all", - { - "selector": str, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_content", - { - "html": str, - "timeout": float, - }, - ), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "text_content", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("title", {}), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "wait_for_function", - { - "expression": str, - "polling": float, - "timeout": float, - }, - ), - ( - "wait_for_load_state", - { - "timeout": float, - }, - ), - ( - "wait_for_selector", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "wait_for_timeout", - { - "timeout": float, - }, - ), - ( - "wait_for_url", - { - "timeout": float, - "url": str, - }, - ), - ), - playwright.async_api.ElementHandle: ( - ("bounding_box", {}), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("content_frame", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "type": str, - }, - ), - ("dispose", {}), - ( - "eval_on_selector", - { - "expression": str, - "selector": str, - }, - ), - ( - "eval_on_selector_all", - { - "expression": str, - "selector": str, - }, - ), - ( - "evaluate", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ("focus", {}), - ( - "get_attribute", - { - "name": str, - }, - ), - ("get_properties", {}), - ( - "get_property", - { - "property_name": str, - }, - ), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("inner_html", {}), - ("inner_text", {}), - ( - "input_value", - { - "timeout": float, - }, - ), - ("is_checked", {}), - ("is_disabled", {}), - ("is_editable", {}), - ("is_enabled", {}), - ("is_hidden", {}), - ("is_visible", {}), - ("json_value", {}), - ("owner_frame", {}), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "query_selector", - { - "selector": str, - }, - ), - ( - "query_selector_all", - { - "selector": str, - }, - ), - ( - "screenshot", - { - "mask_color": str, - "omit_background": bool, - "path": str, - "quality": int, - "style": str, - "timeout": float, - }, - ), - ( - "scroll_into_view_if_needed", - { - "timeout": float, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ( - "select_text", - { - "force": bool, - "timeout": float, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("text_content", {}), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "wait_for_element_state", - { - "timeout": float, - }, - ), - ( - "wait_for_selector", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ), - playwright.async_api.Locator: ( - ("all", {}), - ("all_inner_texts", {}), - ("all_text_contents", {}), - ( - "aria_snapshot", - { - "ref": bool, - "timeout": float, - }, - ), - ( - "blur", - { - "timeout": float, - }, - ), - ( - "bounding_box", - { - "timeout": float, - }, - ), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "clear", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("count", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "timeout": float, - "type": str, - }, - ), - ( - "drag_to", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "element_handle", - { - "timeout": float, - }, - ), - ("element_handles", {}), - ( - "evaluate", - { - "expression": str, - "timeout": float, - }, - ), - ( - "evaluate_all", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - "timeout": float, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ( - "focus", - { - "timeout": float, - }, - ), - ( - "get_attribute", - { - "name": str, - "timeout": float, - }, - ), - ("highlight", {}), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "inner_html", - { - "timeout": float, - }, - ), - ( - "inner_text", - { - "timeout": float, - }, - ), - ( - "input_value", - { - "timeout": float, - }, - ), - ( - "is_checked", - { - "timeout": float, - }, - ), - ( - "is_disabled", - { - "timeout": float, - }, - ), - ( - "is_editable", - { - "timeout": float, - }, - ), - ( - "is_enabled", - { - "timeout": float, - }, - ), - ( - "is_hidden", - { - "timeout": float, - }, - ), - ( - "is_visible", - { - "timeout": float, - }, - ), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "press_sequentially", - { - "delay": float, - "no_wait_after": bool, - "text": str, - "timeout": float, - }, - ), - ( - "screenshot", - { - "mask_color": str, - "omit_background": bool, - "path": str, - "quality": int, - "style": str, - "timeout": float, - }, - ), - ( - "scroll_into_view_if_needed", - { - "timeout": float, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ( - "select_text", - { - "force": bool, - "timeout": float, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "text_content", - { - "timeout": float, - }, - ), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "wait_for", - { - "timeout": float, - }, - ), - ), - playwright.sync_api.BrowserType: ( - ( - "connect", - { - "expose_network": str, - "slow_mo": float, - "timeout": float, - "ws_endpoint": str, - }, - ), - ( - "connect_over_cdp", - { - "endpoint_url": str, - "slow_mo": float, - "timeout": float, - }, - ), - ( - "launch", - { - "channel": str, - "chromium_sandbox": bool, - "devtools": bool, - "downloads_path": str, - "executable_path": str, - "handle_sighup": bool, - "handle_sigint": bool, - "handle_sigterm": bool, - "headless": bool, - "ignore_default_args": bool, - "slow_mo": float, - "timeout": float, - "traces_dir": str, - }, - ), - ( - "launch_persistent_context", - { - "accept_downloads": bool, - "base_url": str, - "bypass_csp": bool, - "channel": str, - "chromium_sandbox": bool, - "device_scale_factor": float, - "devtools": bool, - "downloads_path": str, - "executable_path": str, - "handle_sighup": bool, - "handle_sigint": bool, - "handle_sigterm": bool, - "has_touch": bool, - "headless": bool, - "ignore_default_args": bool, - "ignore_https_errors": bool, - "is_mobile": bool, - "java_script_enabled": bool, - "locale": str, - "no_viewport": bool, - "offline": bool, - "record_har_omit_content": bool, - "record_har_path": str, - "record_har_url_filter": str, - "record_video_dir": str, - "slow_mo": float, - "strict_selectors": bool, - "timeout": float, - "timezone_id": str, - "traces_dir": str, - "user_agent": str, - "user_data_dir": str, - }, - ), - ), - playwright.sync_api.Page: ( - ( - "add_init_script", - { - "path": str, - "script": str, - }, - ), - ( - "add_locator_handler", - { - "no_wait_after": bool, - "times": int, - }, - ), - ( - "add_script_tag", - { - "content": str, - "path": str, - "type": str, - "url": str, - }, - ), - ( - "add_style_tag", - { - "content": str, - "path": str, - "url": str, - }, - ), - ("bring_to_front", {}), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "close", - { - "reason": str, - "run_before_unload": bool, - }, - ), - ("content", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "selector": str, - "strict": bool, - "timeout": float, - "type": str, - }, - ), - ( - "drag_and_drop", - { - "force": bool, - "no_wait_after": bool, - "source": str, - "strict": bool, - "target": str, - "timeout": float, - "trial": bool, - }, - ), - ("emulate_media", {}), - ( - "eval_on_selector", - { - "expression": str, - "selector": str, - "strict": bool, - }, - ), - ( - "eval_on_selector_all", - { - "expression": str, - "selector": str, - }, - ), - ( - "evaluate", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - }, - ), - ( - "expose_binding", - { - "handle": bool, - "name": str, - }, - ), - ( - "expose_function", - { - "name": str, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "focus", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "get_attribute", - { - "name": str, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "go_back", - { - "timeout": float, - }, - ), - ( - "go_forward", - { - "timeout": float, - }, - ), - ( - "goto", - { - "referer": str, - "timeout": float, - "url": str, - }, - ), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "inner_html", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "inner_text", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "input_value", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_checked", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_disabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_editable", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_enabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_hidden", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_visible", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("opener", {}), - ("pause", {}), - ( - "pdf", - { - "display_header_footer": bool, - "footer_template": str, - "format": str, - "header_template": str, - "landscape": bool, - "outline": bool, - "page_ranges": str, - "path": str, - "prefer_css_page_size": bool, - "print_background": bool, - "scale": float, - "tagged": bool, - }, - ), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "query_selector", - { - "selector": str, - "strict": bool, - }, - ), - ( - "query_selector_all", - { - "selector": str, - }, - ), - ( - "reload", - { - "timeout": float, - }, - ), - ("remove_locator_handler", {}), - ("request_gc", {}), - ( - "route", - { - "times": int, - "url": str, - }, - ), - ( - "route_from_har", - { - "har": str, - "update": bool, - "url": str, - }, - ), - ( - "route_web_socket", - { - "url": str, - }, - ), - ( - "screenshot", - { - "full_page": bool, - "mask_color": str, - "omit_background": bool, - "path": str, - "quality": int, - "style": str, - "timeout": float, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_content", - { - "html": str, - "timeout": float, - }, - ), - ("set_extra_http_headers", {}), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("set_viewport_size", {}), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "text_content", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("title", {}), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "unroute", - { - "url": str, - }, - ), - ("unroute_all", {}), - ( - "wait_for_event", - { - "event": str, - "timeout": float, - }, - ), - ( - "wait_for_function", - { - "expression": str, - "polling": float, - "timeout": float, - }, - ), - ( - "wait_for_load_state", - { - "timeout": float, - }, - ), - ( - "wait_for_selector", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "wait_for_timeout", - { - "timeout": float, - }, - ), - ( - "wait_for_url", - { - "timeout": float, - "url": str, - }, - ), - ), - playwright.sync_api.Browser: ( - ( - "close", - { - "reason": str, - }, - ), - ("new_browser_cdp_session", {}), - ( - "new_context", - { - "accept_downloads": bool, - "base_url": str, - "bypass_csp": bool, - "default_browser_type": str, - "device_scale_factor": float, - "has_touch": bool, - "ignore_https_errors": bool, - "is_mobile": bool, - "java_script_enabled": bool, - "locale": str, - "no_viewport": bool, - "offline": bool, - "record_har_omit_content": bool, - "record_har_path": str, - "record_har_url_filter": str, - "record_video_dir": str, - "storage_state": str, - "strict_selectors": bool, - "timezone_id": str, - "user_agent": str, - }, - ), - ( - "new_page", - { - "accept_downloads": bool, - "base_url": str, - "bypass_csp": bool, - "default_browser_type": str, - "device_scale_factor": float, - "has_touch": bool, - "ignore_https_errors": bool, - "is_mobile": bool, - "java_script_enabled": bool, - "locale": str, - "no_viewport": bool, - "offline": bool, - "record_har_omit_content": bool, - "record_har_path": str, - "record_har_url_filter": str, - "record_video_dir": str, - "storage_state": str, - "strict_selectors": bool, - "timezone_id": str, - "user_agent": str, - }, - ), - ( - "start_tracing", - { - "path": str, - "screenshots": bool, - }, - ), - ("stop_tracing", {}), - ), - playwright.sync_api.Frame: ( - ( - "add_script_tag", - { - "content": str, - "path": str, - "type": str, - "url": str, - }, - ), - ( - "add_style_tag", - { - "content": str, - "path": str, - "url": str, - }, - ), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ("content", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "selector": str, - "strict": bool, - "timeout": float, - "type": str, - }, - ), - ( - "drag_and_drop", - { - "force": bool, - "no_wait_after": bool, - "source": str, - "strict": bool, - "target": str, - "timeout": float, - "trial": bool, - }, - ), - ( - "eval_on_selector", - { - "expression": str, - "selector": str, - "strict": bool, - }, - ), - ( - "eval_on_selector_all", - { - "expression": str, - "selector": str, - }, - ), - ( - "evaluate", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "focus", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("frame_element", {}), - ( - "get_attribute", - { - "name": str, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "goto", - { - "referer": str, - "timeout": float, - "url": str, - }, - ), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "inner_html", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "inner_text", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "input_value", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_checked", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_disabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_editable", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_enabled", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_hidden", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "is_visible", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "query_selector", - { - "selector": str, - "strict": bool, - }, - ), - ( - "query_selector_all", - { - "selector": str, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "value": str, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_content", - { - "html": str, - "timeout": float, - }, - ), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "text_content", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ("title", {}), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "selector": str, - "strict": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "wait_for_function", - { - "expression": str, - "polling": float, - "timeout": float, - }, - ), - ( - "wait_for_load_state", - { - "timeout": float, - }, - ), - ( - "wait_for_selector", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ( - "wait_for_timeout", - { - "timeout": float, - }, - ), - ( - "wait_for_url", - { - "timeout": float, - "url": str, - }, - ), - ), - playwright.sync_api.ElementHandle: ( - ("bounding_box", {}), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("content_frame", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "type": str, - }, - ), - ("dispose", {}), - ( - "eval_on_selector", - { - "expression": str, - "selector": str, - }, - ), - ( - "eval_on_selector_all", - { - "expression": str, - "selector": str, - }, - ), - ( - "evaluate", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ("focus", {}), - ( - "get_attribute", - { - "name": str, - }, - ), - ("get_properties", {}), - ( - "get_property", - { - "property_name": str, - }, - ), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("inner_html", {}), - ("inner_text", {}), - ( - "input_value", - { - "timeout": float, - }, - ), - ("is_checked", {}), - ("is_disabled", {}), - ("is_editable", {}), - ("is_enabled", {}), - ("is_hidden", {}), - ("is_visible", {}), - ("json_value", {}), - ("owner_frame", {}), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "query_selector", - { - "selector": str, - }, - ), - ( - "query_selector_all", - { - "selector": str, - }, - ), - ( - "screenshot", - { - "mask_color": str, - "omit_background": bool, - "path": str, - "quality": int, - "style": str, - "timeout": float, - }, - ), - ( - "scroll_into_view_if_needed", - { - "timeout": float, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ( - "select_text", - { - "force": bool, - "timeout": float, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("text_content", {}), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "wait_for_element_state", - { - "timeout": float, - }, - ), - ( - "wait_for_selector", - { - "selector": str, - "strict": bool, - "timeout": float, - }, - ), - ), - playwright.sync_api.Locator: ( - ("all", {}), - ("all_inner_texts", {}), - ("all_text_contents", {}), - ( - "aria_snapshot", - { - "ref": bool, - "timeout": float, - }, - ), - ( - "blur", - { - "timeout": float, - }, - ), - ( - "bounding_box", - { - "timeout": float, - }, - ), - ( - "check", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "clear", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "click", - { - "click_count": int, - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ("count", {}), - ( - "dblclick", - { - "delay": float, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "dispatch_event", - { - "timeout": float, - "type": str, - }, - ), - ( - "drag_to", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "element_handle", - { - "timeout": float, - }, - ), - ("element_handles", {}), - ( - "evaluate", - { - "expression": str, - "timeout": float, - }, - ), - ( - "evaluate_all", - { - "expression": str, - }, - ), - ( - "evaluate_handle", - { - "expression": str, - "timeout": float, - }, - ), - ( - "fill", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ( - "focus", - { - "timeout": float, - }, - ), - ( - "get_attribute", - { - "name": str, - "timeout": float, - }, - ), - ("highlight", {}), - ( - "hover", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "inner_html", - { - "timeout": float, - }, - ), - ( - "inner_text", - { - "timeout": float, - }, - ), - ( - "input_value", - { - "timeout": float, - }, - ), - ( - "is_checked", - { - "timeout": float, - }, - ), - ( - "is_disabled", - { - "timeout": float, - }, - ), - ( - "is_editable", - { - "timeout": float, - }, - ), - ( - "is_enabled", - { - "timeout": float, - }, - ), - ( - "is_hidden", - { - "timeout": float, - }, - ), - ( - "is_visible", - { - "timeout": float, - }, - ), - ( - "press", - { - "delay": float, - "key": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "press_sequentially", - { - "delay": float, - "no_wait_after": bool, - "text": str, - "timeout": float, - }, - ), - ( - "screenshot", - { - "mask_color": str, - "omit_background": bool, - "path": str, - "quality": int, - "style": str, - "timeout": float, - }, - ), - ( - "scroll_into_view_if_needed", - { - "timeout": float, - }, - ), - ( - "select_option", - { - "force": bool, - "index": int, - "label": str, - "no_wait_after": bool, - "timeout": float, - "value": str, - }, - ), - ( - "select_text", - { - "force": bool, - "timeout": float, - }, - ), - ( - "set_checked", - { - "checked": bool, - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "set_input_files", - { - "files": str, - "no_wait_after": bool, - "timeout": float, - }, - ), - ( - "tap", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "text_content", - { - "timeout": float, - }, - ), - ( - "type", - { - "delay": float, - "no_wait_after": bool, - "text": str, - "timeout": float, - }, - ), - ( - "uncheck", - { - "force": bool, - "no_wait_after": bool, - "timeout": float, - "trial": bool, - }, - ), - ( - "wait_for", - { - "timeout": float, - }, - ), - ), -} -# END:METHODS +# Runtime discovery of instrumentable methods and attribute constructors + +_PRIMITIVES: set[Type] = {str, int, float, bool} +_CONVERTIBLE: dict[Type, Type] = {Path: str} + + +def _attr_constructor(annotation: Any) -> Type | None: + if annotation in _PRIMITIVES: + return annotation + if (base := _CONVERTIBLE.get(annotation)) is not None: + return base + if typing.get_origin(annotation) is typing.Union: + return _attr_constructor_for_union(annotation) + return None + + +def _attr_constructor_for_union(annotation: Any) -> Type | None: + assert typing.get_origin(annotation) is typing.Union + args = [a for a in typing.get_args(annotation) if a != type(None)] + if len(args) == 2 and type(None) in args: + # Optional[T] -> T + return _attr_constructor(args[0]) + constructors = [_attr_constructor(a) for a in args] + constructors = [c for c in constructors if c is not None] + if len(constructors) == 0: + return None + first, *rest = constructors + return first if all(c == first for c in rest) else None + + +@dataclass(frozen=True, kw_only=True) +class Method: + name: str + attrs: Mapping[str, AttrConstructor] + + +def _discover_methods_for_type( + target_type: Type, +) -> Sequence[Method]: + discovered: list[Method] = [] + for member_name, member in inspect.getmembers(target_type): + if not inspect.iscoroutinefunction(member) or member_name.startswith("_"): + continue + signature = inspect.signature(member) + attrs: dict[str, AttrConstructor] = {} + for param_name, param in signature.parameters.items(): + if param_name == "self" or param.annotation is inspect.Parameter.empty: + continue + constructor = _attr_constructor(param.annotation) + if constructor is not None: + attrs[param_name] = constructor + discovered.append(Method(name=member_name, attrs=dict(sorted(attrs.items())))) + + discovered.sort(key=lambda m: m.name) + return discovered + + +@functools.cache +def annotated_methods() -> dict[Type, Sequence[Method]]: + """ + Get a mapping of Playwright classes to their instrumentable methods. + """ + methods_map: dict[Type, Sequence[Method]] = {} + async_targets = [ + playwright.async_api.BrowserType, + playwright.async_api.Page, + playwright.async_api.Browser, + playwright.async_api.Frame, + playwright.async_api.ElementHandle, + playwright.async_api.Locator, + ] + for async_type in async_targets: + discovered = _discover_methods_for_type(async_type) + methods_map[async_type] = discovered + # Mirror to sync API using the same class name + sync_type = getattr(playwright.sync_api, async_type.__name__) + assert isinstance(sync_type, type) + methods_map[sync_type] = discovered + return methods_map diff --git a/tests/test_integration.py b/tests/test_integration.py index 0b7443c..5370a83 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -7,7 +7,6 @@ from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace import StatusCode -from opentelemetry.util.types import Attributes from pytest_insta import SnapshotFixture from opentelemetry.instrumentation.playwright import PlaywrightInstrumentor diff --git a/tests/test_targets.py b/tests/test_targets.py deleted file mode 100644 index 2dcae50..0000000 --- a/tests/test_targets.py +++ /dev/null @@ -1,306 +0,0 @@ -import inspect -import typing -from collections.abc import Sequence -from contextlib import contextmanager -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Optional, Type, Union - -import black -import playwright.async_api -import pytest - - -@dataclass -class Target: - type: Type - exclude: Sequence[str] = () - - @property - def name(self) -> str: - return self.type.__name__ - - -@dataclass -class Attr: - name: str - type: Type - - -@dataclass -class Method: - name: str - attrs: Sequence[Attr] - - -@dataclass -class Instrumentation: - module: str - type_name: str - methods: Sequence[Method] - - -TARGETS = [ - Target(playwright.async_api.BrowserType), - Target(playwright.async_api.Page), - Target(playwright.async_api.Browser), - Target(playwright.async_api.Frame), - Target(playwright.async_api.ElementHandle), - Target(playwright.async_api.Locator), -] - - -def test_targets_list_is_up_to_date(): - """ - The Playwright API is massive, so instead of manually maintaining a list of - methods to instrument, we'll use reflection and codegen to build it. - - In general, we only want to instrument "slow" functions that have - side-effects. Once nice aspect of the async API is that these functions are - all marked as `async` functions. - - From there, we can use type hints to figure out the arguments that we can - attach to the span. - - Once we've done all of that, we'll generate a big `*.py` file which gets - consumed by the instrumentor to do our monkeypatching. - """ - methods_by_type = {target.name: methods(target) for target in TARGETS} - - instrumented = [ - Instrumentation( - module=module, - type_name=target.name, - methods=methods_by_type[target.name], - ) - for module in ["playwright.async_api", "playwright.sync_api"] - for target in TARGETS - ] - - rendered = render(instrumented) - - root_dir = next(p for p in Path(__file__).parents if p.joinpath("uv.lock").exists()) - targets_py = root_dir.joinpath( - "src", "opentelemetry", "instrumentation", "playwright", "targets.py" - ) - original_text = targets_py.read_text() - expected = update_tagged_section(original_text, "METHODS", rendered) - expected = black.format_str(expected, mode=black.Mode()) - ensure_file_contains(targets_py, expected) - - -def ensure_file_contains(path: Path, expected: str): - src = path.read_text() - if expected in src: - return - - _ = path.write_text(expected) - raise AssertionError( - f'Updated "{path}" to contain "{expected}". Please commit the changes and re-run the test.' - ) - - -def update_tagged_section(text: str, tag: str, content: str) -> str: - """ - Look for a section of text between `# START:{tag}` and `# END:{tag}`, and - replace it with the given content. - - If no such section is found, append the content to the end of the text. - """ - start_tag = f"# START:{tag}" - end_tag = f"# END:{tag}" - lines = text.splitlines() - - # Find the start and end indices of the tagged section - start_idx = -1 - end_idx = -1 - for i, line in enumerate(lines): - if line.strip() == start_tag: - start_idx = i - elif line.strip() == end_tag: - end_idx = i - break - - # If we found a tagged section, replace it - if start_idx >= 0 and end_idx >= 0: - new_lines = lines[: start_idx + 1] - new_lines.extend(content.splitlines()) - new_lines.extend(lines[end_idx:]) - return "\n".join(new_lines) - - # Otherwise append the content with tags - lines.extend(["", start_tag, *content.splitlines(), end_tag]) - return "\n".join(lines) - - -def methods(target: Target) -> Sequence[Method]: - all_methods: list[Method] = [] - - for member_name, member in inspect.getmembers(target.type): - if not inspect.iscoroutinefunction(member) or member_name.startswith("_"): - continue - - signature = inspect.signature(member) - attrs: list[Attr] = [] - - for param_name, param in signature.parameters.items(): - if param_name == "self" or param.annotation is inspect.Parameter.empty: - continue - - if constructor := attr_constructor(param.annotation): - attrs.append(Attr(param_name, constructor)) - - method = Method(member_name, sorted(attrs, key=lambda a: a.name)) - all_methods.append(method) - - return sorted(all_methods, key=lambda m: m.name) - - -primitives: set[Type] = {str, int, float, bool} -convertible: dict[Type, Type] = { - Path: str, -} - - -@pytest.mark.parametrize( - "annotation, expected", - [ - (str, str), - (int, int), - (float, float), - (bool, bool), - (Path, str), - (Optional[str], str), - (Union[str, type(None)], str), - # Ambiguous, can't infer which constructor to use - (Union[str, int], None), - ], -) -def test_attr_constructor(annotation: Any, expected: Type | None): - assert attr_constructor(annotation) == expected - - -def attr_constructor(annotation: Any) -> Type | None: - if annotation in primitives: - return annotation - - if base := convertible.get(annotation): - return base - - if typing.get_origin(annotation) is Union: - return attr_constructor_for_union(annotation) - - return None - - -def attr_constructor_for_union(annotation: Any) -> Type | None: - assert typing.get_origin(annotation) is Union - - args = [a for a in typing.get_args(annotation) if a != type(None)] - - if len(args) == 2 and type(None) in args: - # Treat Optional[T] as T - return attr_constructor(args[0]) - - constructors = [attr_constructor(a) for a in args] - constructors = [c for c in constructors if c is not None] - - if len(constructors) == 0: - return None - - first, *rest = constructors - - # Note: All variants of a union must use the same attribute constructor - # otherwise it's ambiguous - if all(c == first for c in rest): - return first - else: - return None - - -class Renderer: - def __init__(self): - self.buffer: list[str] = [] - self.prefix: str = "" - - def _print(self, value: str): - if self.buffer and self.buffer[-1] == "\n": - self.buffer.append(self.prefix) - - self.buffer.append(value) - - def print(self, *values: str): - for value in values: - self._print(value) - - def println(self, *values: str): - self.print(*values) - self.newline() - - def newline(self): - self.buffer.append("\n") - - @contextmanager - def indent(self, indent: str = " "): - old_prefix = self.prefix - self.prefix += indent - try: - yield - finally: - self.prefix = old_prefix - - def write(self, text: str): - self.buffer.append(self.prefix + text) - - def finish(self) -> str: - return "".join(self.buffer) - - -def render_instrumentation(r: Renderer, instrumentation: Instrumentation): - r.println(instrumentation.module, ".", instrumentation.type_name, ": (") - - with r.indent(): - for method in instrumentation.methods: - render_method(r, method) - - r.println("),") - - -def render_method(r: Renderer, method: Method): - r.print("(") - - with r.indent(): - r.print('"', method.name, '", ') - render_attrs(r, method.attrs) - - r.println("),") - - -def render_attrs(r: Renderer, attrs: Sequence[Attr]): - r.print("{") - - with r.indent(): - for attr in attrs: - render_attr(r, attr) - r.print(", ") - - r.print("}") - - -def render_attr(r: Renderer, attr: Attr): - r.print('"', attr.name, '": ', attr.type.__name__) - - -def render(instrumented: Sequence[Instrumentation]) -> str: - r = Renderer() - r.println( - "METHODS: dict[Type, tuple[tuple[str, dict[str, AttrConstructor]], ...]] = {" - ) - - with r.indent(): - for instrumentation in instrumented: - render_instrumentation(r, instrumentation) - - r.println("}") - - return r.finish()