diff --git a/playwright/_impl/_artifact.py b/playwright/_impl/_artifact.py index a08294cbe..9bfcec167 100644 --- a/playwright/_impl/_artifact.py +++ b/playwright/_impl/_artifact.py @@ -17,7 +17,8 @@ from typing import Dict, Optional, Union, cast from playwright._impl._connection import ChannelOwner, from_channel -from playwright._impl._helper import Error, make_dirs_for_file, patch_error_message +from playwright._impl._errors import Error, patch_error_message +from playwright._impl._helper import make_dirs_for_file from playwright._impl._stream import Stream diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index d3ec27215..f3c84ee24 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -20,16 +20,16 @@ from playwright._impl._connection import format_call_log from playwright._impl._errors import Error from playwright._impl._fetch import APIResponse -from playwright._impl._helper import is_textual_mime_type from playwright._impl._internal_structures import ( AriaRole, ExpectedTextValue, FrameExpectOptions, FrameExpectResult, ) -from playwright._impl._js_handle import parse_value from playwright._impl._locator import Locator +from playwright._impl._mime_type import is_textual_mime_type from playwright._impl._page import Page +from playwright._impl._serializers import parse_value from playwright._impl._str_utils import escape_regex_flags _soft_errors: Optional[List[AssertionError]] = None diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index dc6c21d2f..73b4a092a 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -50,25 +50,24 @@ from playwright._impl._debugger import Debugger from playwright._impl._dialog import Dialog from playwright._impl._disposable import Disposable, DisposableStub -from playwright._impl._errors import Error, TargetClosedError +from playwright._impl._errors import Error, TargetClosedError, parse_error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._fetch import APIRequestContext from playwright._impl._frame import Frame from playwright._impl._har_router import HarRouter +from playwright._impl._headers import serialize_headers from playwright._impl._helper import ( HarContentPolicy, HarMode, RouteFromHarNotFoundPolicy, RouteHandler, RouteHandlerCallback, - TimeoutSettings, URLMatch, WebSocketRouteHandlerCallback, async_readfile, async_writefile, create_task_and_ignore_exception, locals_to_params, - parse_error, to_impl, ) from playwright._impl._network import ( @@ -77,10 +76,10 @@ Route, WebSocketRoute, WebSocketRouteHandler, - serialize_headers, to_clear_cookies_params, ) from playwright._impl._page import BindingCall, Page, Worker +from playwright._impl._timeout_settings import TimeoutSettings from playwright._impl._tracing import Tracing from playwright._impl._waiter import Waiter from playwright._impl._web_error import WebError diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index a4632cca2..80ba45a28 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -29,6 +29,7 @@ from playwright._impl._browser_context import BrowserContext from playwright._impl._connection import ChannelOwner, Connection, from_channel from playwright._impl._errors import Error +from playwright._impl._headers import serialize_headers from playwright._impl._helper import ( PLAYWRIGHT_MAX_DEADLINE, ColorScheme, @@ -39,16 +40,15 @@ HarMode, ReducedMotion, ServiceWorkersPolicy, - TimeoutSettings, async_readfile, locals_to_params, ) from playwright._impl._json_pipe import JsonPipeTransport from playwright._impl._network import ( - serialize_headers, to_client_certificates_protocol, to_http_credentials_protocol, ) +from playwright._impl._timeout_settings import TimeoutSettings from playwright._impl._waiter import throw_on_timeout if TYPE_CHECKING: diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index cf116471a..c0b01b2cc 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -40,13 +40,16 @@ import playwright import playwright._impl._impl_to_api_mapping -from playwright._impl._errors import TargetClosedError, rewrite_error +from playwright._impl._errors import ( + Error, + TargetClosedError, + parse_error, + rewrite_error, +) from playwright._impl._greenlets import EventGreenlet from playwright._impl._helper import ( - Error, ParsedMessagePayload, create_task_and_ignore_exception, - parse_error, ) from playwright._impl._protocol_check import check_message, is_enabled from playwright._impl._transport import Transport diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py index 2284b96e3..c01dd4b44 100644 --- a/playwright/_impl/_element_handle.py +++ b/playwright/_impl/_element_handle.py @@ -29,8 +29,8 @@ from playwright._impl._api_structures import FilePayload, FloatRect, Position from playwright._impl._connection import ChannelOwner, from_nullable_channel +from playwright._impl._errors import Error from playwright._impl._helper import ( - Error, KeyboardModifier, MouseButton, async_writefile, diff --git a/playwright/_impl/_errors.py b/playwright/_impl/_errors.py index 9630a2390..3bc1d0127 100644 --- a/playwright/_impl/_errors.py +++ b/playwright/_impl/_errors.py @@ -12,11 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Port of packages/playwright-core/src/client/errors.ts.""" + # These are types that we use in the API. They are public and are a part of the # stable API. -from typing import Any, List, Optional +import re +import traceback +from types import TracebackType +from typing import Any, List, Optional, TypedDict def is_target_closed_error(error: Exception) -> bool: @@ -62,3 +67,49 @@ def rewrite_error(error: Exception, message: str) -> Exception: rewritten_exc._details = error._details rewritten_exc._log = error._log return rewritten_exc + + +class ErrorPayload(TypedDict, total=False): + message: str + name: str + stack: str + value: Optional[Any] + + +def serialize_error(ex: Exception, tb: Optional[TracebackType]) -> ErrorPayload: + return ErrorPayload( + message=str(ex), name="Error", stack="".join(traceback.format_tb(tb)) + ) + + +def parse_error(error: ErrorPayload, log: Optional[str] = None) -> Error: + base_error_class = Error + if error.get("name") == "TimeoutError": + base_error_class = TimeoutError + if error.get("name") == "TargetClosedError": + base_error_class = TargetClosedError + if not log: + log = "" + exc = base_error_class(patch_error_message(error["message"]) + log) + exc._name = error["name"] + exc._stack = error["stack"] + return exc + + +def patch_error_message(message: str) -> str: + match = re.match(r"(\w+)(: expected .*)", message) + if match: + message = to_snake_case(match.group(1)) + match.group(2) + message = message.replace( + "Pass { acceptDownloads: true }", "Pass 'accept_downloads=True'" + ) + return message + + +# Upstream: toSnakeCase() in packages/isomorphic/stringUtils.ts. It is here, not +# in _str_utils.py, because _str_utils.py imports this module. +to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") + + +def to_snake_case(name: str) -> str: + return to_snake_case_regex.sub(r"_\1", name).lower() diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index 624d5f1b0..1d7fd53ea 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -34,27 +34,28 @@ StorageState, ) from playwright._impl._connection import ChannelOwner, from_channel -from playwright._impl._errors import is_target_closed_error +from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error from playwright._impl._form_data import FormData +from playwright._impl._headers import serialize_headers from playwright._impl._helper import ( - Error, - NameValue, - TargetClosedError, - TimeoutSettings, async_readfile, async_writefile, is_file_payload, locals_to_params, - object_to_array, to_impl, ) -from playwright._impl._internal_structures import FormField, Headers, ServerFilePayload +from playwright._impl._internal_structures import ( + FormField, + Headers, + NameValue, + ServerFilePayload, +) from playwright._impl._network import ( - serialize_headers, to_clear_cookies_params, to_client_certificates_protocol, to_http_credentials_protocol, ) +from playwright._impl._timeout_settings import TimeoutSettings from playwright._impl._tracing import Tracing if typing.TYPE_CHECKING: @@ -493,6 +494,15 @@ async def storage_state( return result +def object_to_array(obj: Optional[Dict]) -> Optional[List[NameValue]]: + if not obj: + return None + result = [] + for key, value in obj.items(): + result.append(NameValue(name=key, value=str(value))) + return result + + def file_payload_to_json(payload: FilePayload) -> ServerFilePayload: return ServerFilePayload( name=payload["name"], diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index ad7f489d4..ab554ca41 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -44,11 +44,9 @@ FrameNavigatedEvent, KeyboardModifier, MouseButton, - TimeoutSettings, URLMatch, async_readfile, locals_to_params, - monotonic_time, url_matches, ) from playwright._impl._internal_structures import ( @@ -61,13 +59,15 @@ Serializable, add_source_url_to_script, parse_result, - parse_value, serialize_argument, ) from playwright._impl._locator import ( ANY_FRAME_SELECTOR, FrameLocator, Locator, + test_id_attribute_name, +) +from playwright._impl._locator_utils import ( get_by_alt_text_selector, get_by_label_selector, get_by_placeholder_selector, @@ -75,10 +75,12 @@ get_by_test_id_selector, get_by_text_selector, get_by_title_selector, - test_id_attribute_name, ) from playwright._impl._network import Response +from playwright._impl._serializers import parse_value from playwright._impl._set_input_files_helpers import convert_input_files +from playwright._impl._time import monotonic_time +from playwright._impl._timeout_settings import TimeoutSettings from playwright._impl._waiter import Waiter from playwright._impl._web_mcp import WebMCP diff --git a/playwright/_impl/_headers.py b/playwright/_impl/_headers.py new file mode 100644 index 000000000..96245f2cd --- /dev/null +++ b/playwright/_impl/_headers.py @@ -0,0 +1,28 @@ +# 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. + +"""Port of packages/isomorphic/headers.ts.""" + +from typing import Dict + +from playwright._impl._internal_structures import HeadersArray + + +# Upstream: headersObjectToArray(). +def serialize_headers(headers: Dict[str, str]) -> HeadersArray: + return [ + {"name": name, "value": value} + for name, value in headers.items() + if value is not None + ] diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 1bdb5a591..5a0e182fb 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -16,10 +16,7 @@ import math import os import re -import time -import traceback from pathlib import Path -from types import TracebackType from typing import ( TYPE_CHECKING, Any, @@ -38,15 +35,8 @@ overload, ) -from playwright._impl._errors import ( - Error, - TargetClosedError, - TimeoutError, - is_target_closed_error, - rewrite_error, -) +from playwright._impl._errors import ErrorPayload, is_target_closed_error, rewrite_error from playwright._impl._greenlets import RouteGreenlet -from playwright._impl._internal_structures import NameValue from playwright._impl._str_utils import escape_regex_flags from playwright._impl._url_match import URLMatch, url_matches @@ -78,13 +68,6 @@ RouteFromHarNotFoundPolicy = Literal["abort", "fallback"] -class ErrorPayload(TypedDict, total=False): - message: str - name: str - stack: str - value: Optional[Any] - - class HarRecordingMetadata(TypedDict, total=False): path: str content: Optional[HarContentPolicy] @@ -159,91 +142,9 @@ class HarLookupResult(TypedDict, total=False): body: Optional[str] -DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS = 30000 -DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS = 180000 PLAYWRIGHT_MAX_DEADLINE = 2147483647 # 2^31-1 -class TimeoutSettings: - - @staticmethod - def launch_timeout(timeout: Optional[float] = None) -> float: - return ( - timeout - if timeout is not None - else DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS - ) - - def __init__(self, parent: Optional["TimeoutSettings"]) -> None: - self._parent = parent - self._default_timeout: Optional[float] = None - self._default_navigation_timeout: Optional[float] = None - - def set_default_timeout(self, timeout: Optional[float]) -> None: - self._default_timeout = timeout - - def timeout(self, timeout: float = None) -> float: - if timeout is not None: - return timeout - if self._default_timeout is not None: - return self._default_timeout - if self._parent: - return self._parent.timeout() - return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS - - def set_default_navigation_timeout( - self, navigation_timeout: Optional[float] - ) -> None: - self._default_navigation_timeout = navigation_timeout - - def default_navigation_timeout(self) -> Optional[float]: - return self._default_navigation_timeout - - def default_timeout(self) -> Optional[float]: - return self._default_timeout - - def navigation_timeout(self, timeout: float = None) -> float: - if timeout is not None: - return timeout - if self._default_navigation_timeout is not None: - return self._default_navigation_timeout - if self._default_timeout is not None: - return self._default_timeout - if self._parent: - return self._parent.navigation_timeout() - return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS - - -def serialize_error(ex: Exception, tb: Optional[TracebackType]) -> ErrorPayload: - return ErrorPayload( - message=str(ex), name="Error", stack="".join(traceback.format_tb(tb)) - ) - - -def parse_error(error: ErrorPayload, log: Optional[str] = None) -> Error: - base_error_class = Error - if error.get("name") == "TimeoutError": - base_error_class = TimeoutError - if error.get("name") == "TargetClosedError": - base_error_class = TargetClosedError - if not log: - log = "" - exc = base_error_class(patch_error_message(error["message"]) + log) - exc._name = error["name"] - exc._stack = error["stack"] - return exc - - -def patch_error_message(message: str) -> str: - match = re.match(r"(\w+)(: expected .*)", message) - if match: - message = to_snake_case(match.group(1)) + match.group(2) - message = message.replace( - "Pass { acceptDownloads: true }", "Pass 'accept_downloads=True'" - ) - return message - - def locals_to_params(args: Dict) -> Dict: copy = {} for key in args: @@ -258,10 +159,6 @@ def locals_to_params(args: Dict) -> Dict: return copy -def monotonic_time() -> int: - return math.floor(time.monotonic() * 1000) - - def create_task_and_ignore_exception( loop: asyncio.AbstractEventLoop, coro: Awaitable[Any] ) -> None: @@ -395,13 +292,6 @@ def prepare_interception_patterns( return patterns -to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") - - -def to_snake_case(name: str) -> str: - return to_snake_case_regex.sub(r"_\1", name).lower() - - def make_dirs_for_file(path: Union[Path, str]) -> None: if not os.path.isabs(path): path = Path.cwd() / path @@ -435,15 +325,6 @@ def to_impl(obj: T) -> T: return obj -def object_to_array(obj: Optional[Dict]) -> Optional[List[NameValue]]: - if not obj: - return None - result = [] - for key, value in obj.items(): - result.append(NameValue(name=key, value=str(value))) - return result - - def is_file_payload(value: Optional[Any]) -> bool: return ( isinstance(value, dict) @@ -453,15 +334,6 @@ def is_file_payload(value: Optional[Any]) -> bool: ) -TEXTUAL_MIME_TYPE = re.compile( - r"^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$" -) - - -def is_textual_mime_type(mime_type: str) -> bool: - return bool(TEXTUAL_MIME_TYPE.match(mime_type)) - - @overload def to_milliseconds(value: Union[float, datetime.timedelta]) -> float: ... diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index 49a32ee27..296aa7ca5 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -12,21 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import base64 -import collections.abc -import datetime -import math -import re -import struct -import traceback +"""Port of packages/playwright-core/src/client/jsHandle.ts.""" + from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Pattern, Union -from urllib.parse import ParseResult, urlparse, urlunparse +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union 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 +from playwright._impl._errors import is_target_closed_error +from playwright._impl._serializers import parse_value, serialize_value if TYPE_CHECKING: # pragma: no cover from playwright._impl._element_handle import ElementHandle @@ -35,21 +28,6 @@ Serializable = Any -class VisitorInfo: - visited: Map[Any, int] - last_id: int - - def __init__(self) -> None: - self.visited = Map() - self.last_id = 0 - - def visit(self, obj: Any) -> int: - assert obj not in self.visited - self.last_id += 1 - self.visited[obj] = self.last_id - return self.last_id - - class JSHandle(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict @@ -131,214 +109,17 @@ async def json_value(self) -> Any: ) -def serialize_value( - value: Any, handles: List[Channel], visitor_info: Optional[VisitorInfo] = None -) -> Any: - if visitor_info is None: - visitor_info = VisitorInfo() - if isinstance(value, JSHandle): - h = len(handles) - handles.append(value._channel) - return dict(h=h) - if value is None: - return dict(v="null") - if isinstance(value, float): - if value == float("inf"): - return dict(v="Infinity") - if value == float("-inf"): - return dict(v="-Infinity") - if value == 0 and math.copysign(1.0, value) == -1.0: - return dict(v="-0") - if math.isnan(value): - return dict(v="NaN") - if isinstance(value, datetime.datetime): - # Node.js Date objects are always in UTC. - return { - "d": datetime.datetime.strftime( - value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ" - ) - } - if isinstance(value, Exception): - return { - "e": { - "m": str(value), - "n": ( - (value.name or "") - if isinstance(value, Error) - else value.__class__.__name__ - ), - "s": ( - (value.stack or "") - if isinstance(value, Error) - else "".join( - traceback.format_exception(type(value), value=value, tb=None) - ) - ), - } - } - if isinstance(value, bool): - return {"b": value} - if isinstance(value, (int, float)): - return {"n": value} - if isinstance(value, str): - return {"s": value} - if isinstance(value, ParseResult): - return {"u": urlunparse(value)} - if isinstance(value, Pattern): - if not isinstance(value.pattern, str): - raise Error("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]) - - if isinstance(value, collections.abc.Sequence) and not isinstance(value, str): - id = visitor_info.visit(value) - a = [] - for e in value: - a.append(serialize_value(e, handles, visitor_info)) - return dict(a=a, id=id) - - if isinstance(value, dict): - id = visitor_info.visit(value) - o = [] - for name in value: - o.append( - {"k": name, "v": serialize_value(value[name], handles, visitor_info)} - ) - return dict(o=o, id=id) - return dict(v="undefined") - - def serialize_argument(arg: Serializable = None) -> Any: handles: List[Channel] = [] - value = serialize_value(arg, handles) - return dict(value=value, handles=handles) - -def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: - if refs is None: - refs = {} - if value is None: + def serialize_handle(value: Any) -> Optional[Dict[str, Any]]: + if isinstance(value, JSHandle): + handles.append(value._channel) + return {"h": len(handles) - 1} return None - if isinstance(value, dict): - if "ref" in value: - return refs[value["ref"]] - - if "v" in value: - v = value["v"] - if v == "Infinity": - return float("inf") - if v == "-Infinity": - return float("-inf") - if v == "-0": - return float("-0") - if v == "NaN": - return float("nan") - if v == "undefined": - return None - if v == "null": - return None - return v - - if "u" in value: - return urlparse(value["u"]) - - if "bi" in value: - return int(value["bi"]) - - if "e" in value: - error = Error(value["e"]["m"]) - error._name = value["e"]["n"] - 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 - for e in value["a"]: - a.append(parse_value(e, refs)) - return a - if "d" in value: - # Node.js Date objects are always in UTC. - return datetime.datetime.strptime( - value["d"], "%Y-%m-%dT%H:%M:%S.%fZ" - ).replace(tzinfo=datetime.timezone.utc) - - if "o" in value: - o: Dict = {} - refs[value["id"]] = o - for e in value["o"]: - o[e["k"]] = parse_value(e["v"], refs) - return o - - if "n" in value: - return value["n"] - - if "s" in value: - return value["s"] - - if "b" in value: - return value["b"] - - if "ta" in value: - encoded_bytes = value["ta"]["b"] - decoded_bytes = base64.b64decode(encoded_bytes) - array_type = value["ta"]["k"] - if array_type == "i8": - word_size = 1 - fmt = "b" - elif array_type == "ui8" or array_type == "ui8c": - word_size = 1 - fmt = "B" - elif array_type == "i16": - word_size = 2 - fmt = "h" - elif array_type == "ui16": - word_size = 2 - fmt = "H" - elif array_type == "i32": - word_size = 4 - fmt = "i" - elif array_type == "ui32": - word_size = 4 - fmt = "I" - elif array_type == "f32": - word_size = 4 - fmt = "f" - elif array_type == "f64": - word_size = 8 - fmt = "d" - elif array_type == "bi64": - word_size = 8 - fmt = "q" - elif array_type == "bui64": - word_size = 8 - fmt = "Q" - else: - raise ValueError(f"Unsupported array type: {array_type}") - - byte_len = len(decoded_bytes) - if byte_len % word_size != 0: - raise ValueError( - f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}" - ) - - if byte_len == 0: - return [] - array_len = byte_len // word_size - # "<" denotes little-endian - format_string = f"<{array_len}{fmt}" - return list(struct.unpack(format_string, decoded_bytes)) - return value + value = serialize_value(arg, serialize_handle) + return dict(value=value, handles=handles) def parse_result(result: Any) -> Any: diff --git a/playwright/_impl/_json_pipe.py b/playwright/_impl/_json_pipe.py index 41973b8c7..2eec6a32c 100644 --- a/playwright/_impl/_json_pipe.py +++ b/playwright/_impl/_json_pipe.py @@ -18,8 +18,8 @@ from pyee.asyncio import AsyncIOEventEmitter from playwright._impl._connection import Channel -from playwright._impl._errors import TargetClosedError -from playwright._impl._helper import Error, ParsedMessagePayload +from playwright._impl._errors import Error, TargetClosedError +from playwright._impl._helper import ParsedMessagePayload from playwright._impl._transport import Transport diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index a0ddddd75..6d98b2bbd 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -26,7 +26,6 @@ Optional, Pattern, Sequence, - Tuple, TypeVar, Union, ) @@ -39,12 +38,11 @@ ) from playwright._impl._disposable import DisposableStub from playwright._impl._element_handle import ElementHandle +from playwright._impl._errors import Error from playwright._impl._helper import ( - Error, KeyboardModifier, MouseButton, locals_to_params, - monotonic_time, to_impl, ) from playwright._impl._internal_structures import ( @@ -53,10 +51,18 @@ FrameExpectResult, ) from playwright._impl._js_handle import Serializable, serialize_argument -from playwright._impl._str_utils import ( - escape_for_attribute_selector, - escape_for_text_selector, +from playwright._impl._locator_utils import ( + bool_to_js_bool, + get_by_alt_text_selector, + get_by_label_selector, + get_by_placeholder_selector, + get_by_role_selector, + get_by_test_id_selector, + get_by_text_selector, + get_by_title_selector, ) +from playwright._impl._str_utils import escape_for_text_selector +from playwright._impl._time import monotonic_time if TYPE_CHECKING: # pragma: no cover from playwright._impl._frame import Frame @@ -969,87 +975,3 @@ def test_id_attribute_name() -> str: def set_test_id_attribute_name(attribute_name: str) -> None: global _test_id_attribute_name _test_id_attribute_name = attribute_name - - -def get_by_test_id_selector( - test_id_attribute_name: str, test_id: Union[str, Pattern[str]] -) -> str: - return f"internal:testid=[{test_id_attribute_name}={escape_for_attribute_selector(test_id, True)}]" - - -def get_by_attribute_text_selector( - attr_name: str, text: Union[str, Pattern[str]], exact: bool = None -) -> str: - return f"internal:attr=[{attr_name}={escape_for_attribute_selector(text, exact=exact)}]" - - -def get_by_label_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: - return "internal:label=" + escape_for_text_selector(text, exact=exact) - - -def get_by_alt_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: - return get_by_attribute_text_selector("alt", text, exact=exact) - - -def get_by_title_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: - return get_by_attribute_text_selector("title", text, exact=exact) - - -def get_by_placeholder_selector( - text: Union[str, Pattern[str]], exact: bool = None -) -> str: - return get_by_attribute_text_selector("placeholder", text, exact=exact) - - -def get_by_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: - return "internal:text=" + escape_for_text_selector(text, exact=exact) - - -def bool_to_js_bool(value: bool) -> str: - return "true" if value else "false" - - -def get_by_role_selector( - role: AriaRole, - checked: bool = None, - disabled: bool = None, - expanded: bool = None, - includeHidden: bool = None, - level: int = None, - name: Union[str, Pattern[str]] = None, - pressed: bool = None, - selected: bool = None, - exact: bool = None, - description: Union[str, Pattern[str]] = None, -) -> str: - props: List[Tuple[str, str]] = [] - if checked is not None: - props.append(("checked", bool_to_js_bool(checked))) - if disabled is not None: - props.append(("disabled", bool_to_js_bool(disabled))) - if selected is not None: - props.append(("selected", bool_to_js_bool(selected))) - if expanded is not None: - props.append(("expanded", bool_to_js_bool(expanded))) - if includeHidden is not None: - props.append(("include-hidden", bool_to_js_bool(includeHidden))) - if level is not None: - props.append(("level", str(level))) - if name is not None: - props.append( - ( - "name", - escape_for_attribute_selector(name, exact=exact), - ) - ) - if description is not None: - props.append( - ( - "description", - escape_for_attribute_selector(description, exact=exact), - ) - ) - if pressed is not None: - props.append(("pressed", bool_to_js_bool(pressed))) - props_str = "".join([f"[{t[0]}={t[1]}]" for t in props]) - return f"internal:role={role}{props_str}" diff --git a/playwright/_impl/_locator_utils.py b/playwright/_impl/_locator_utils.py new file mode 100644 index 000000000..2ed19143a --- /dev/null +++ b/playwright/_impl/_locator_utils.py @@ -0,0 +1,107 @@ +# 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. + +"""Port of packages/isomorphic/locatorUtils.ts.""" + +from typing import List, Pattern, Tuple, Union + +from playwright._impl._internal_structures import AriaRole +from playwright._impl._str_utils import ( + escape_for_attribute_selector, + escape_for_text_selector, +) + + +def get_by_test_id_selector( + test_id_attribute_name: str, test_id: Union[str, Pattern[str]] +) -> str: + return f"internal:testid=[{test_id_attribute_name}={escape_for_attribute_selector(test_id, True)}]" + + +def get_by_attribute_text_selector( + attr_name: str, text: Union[str, Pattern[str]], exact: bool = None +) -> str: + return f"internal:attr=[{attr_name}={escape_for_attribute_selector(text, exact=exact)}]" + + +def get_by_label_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return "internal:label=" + escape_for_text_selector(text, exact=exact) + + +def get_by_alt_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return get_by_attribute_text_selector("alt", text, exact=exact) + + +def get_by_title_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return get_by_attribute_text_selector("title", text, exact=exact) + + +def get_by_placeholder_selector( + text: Union[str, Pattern[str]], exact: bool = None +) -> str: + return get_by_attribute_text_selector("placeholder", text, exact=exact) + + +def get_by_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return "internal:text=" + escape_for_text_selector(text, exact=exact) + + +def bool_to_js_bool(value: bool) -> str: + return "true" if value else "false" + + +def get_by_role_selector( + role: AriaRole, + checked: bool = None, + disabled: bool = None, + expanded: bool = None, + includeHidden: bool = None, + level: int = None, + name: Union[str, Pattern[str]] = None, + pressed: bool = None, + selected: bool = None, + exact: bool = None, + description: Union[str, Pattern[str]] = None, +) -> str: + props: List[Tuple[str, str]] = [] + if checked is not None: + props.append(("checked", bool_to_js_bool(checked))) + if disabled is not None: + props.append(("disabled", bool_to_js_bool(disabled))) + if selected is not None: + props.append(("selected", bool_to_js_bool(selected))) + if expanded is not None: + props.append(("expanded", bool_to_js_bool(expanded))) + if includeHidden is not None: + props.append(("include-hidden", bool_to_js_bool(includeHidden))) + if level is not None: + props.append(("level", str(level))) + if name is not None: + props.append( + ( + "name", + escape_for_attribute_selector(name, exact=exact), + ) + ) + if description is not None: + props.append( + ( + "description", + escape_for_attribute_selector(description, exact=exact), + ) + ) + if pressed is not None: + props.append(("pressed", bool_to_js_bool(pressed))) + props_str = "".join([f"[{t[0]}={t[1]}]" for t in props]) + return f"internal:role={role}{props_str}" diff --git a/playwright/_impl/_mime_type.py b/playwright/_impl/_mime_type.py new file mode 100644 index 000000000..1e1f6b758 --- /dev/null +++ b/playwright/_impl/_mime_type.py @@ -0,0 +1,25 @@ +# 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. + +"""Port of packages/isomorphic/mimeType.ts.""" + +import re + +TEXTUAL_MIME_TYPE = re.compile( + r"^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$" +) + + +def is_textual_mime_type(mime_type: str) -> bool: + return bool(TEXTUAL_MIME_TYPE.match(mime_type)) diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 589bc45dd..3628df6df 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -52,6 +52,7 @@ ) from playwright._impl._errors import Error from playwright._impl._event_context_manager import EventContextManagerImpl +from playwright._impl._headers import serialize_headers from playwright._impl._helper import ( URLMatch, WebSocketRouteHandlerCallback, @@ -85,14 +86,6 @@ def __init__(self) -> None: self.post_data_buffer: Optional[bytes] = None -def serialize_headers(headers: Dict[str, str]) -> HeadersArray: - return [ - {"name": name, "value": value} - for name, value in headers.items() - if value is not None - ] - - async def to_client_certificates_protocol( clientCertificates: Optional[List[ClientCertificate]], ) -> Optional[List[Dict[str, str]]]: diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index cff2ab606..bd4f359ee 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -52,12 +52,19 @@ from playwright._impl._disposable import Disposable, DisposableStub from playwright._impl._download import Download from playwright._impl._element_handle import ElementHandle, determine_screenshot_type -from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error +from playwright._impl._errors import ( + Error, + TargetClosedError, + is_target_closed_error, + parse_error, + serialize_error, +) from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._file_chooser import FileChooser from playwright._impl._frame import Frame from playwright._impl._greenlets import LocatorHandlerGreenlet from playwright._impl._har_router import HarRouter +from playwright._impl._headers import serialize_headers from playwright._impl._helper import ( ColorScheme, Contrast, @@ -70,7 +77,6 @@ RouteFromHarNotFoundPolicy, RouteHandler, RouteHandlerCallback, - TimeoutSettings, URLMatch, URLMatchRequest, URLMatchResponse, @@ -79,8 +85,6 @@ async_writefile, locals_to_params, make_dirs_for_file, - parse_error, - serialize_error, url_matches, ) from playwright._impl._input import Keyboard, Mouse, Touchscreen @@ -98,10 +102,10 @@ Route, WebSocketRoute, WebSocketRouteHandler, - serialize_headers, ) from playwright._impl._screencast import Screencast from playwright._impl._signature import signature +from playwright._impl._timeout_settings import TimeoutSettings from playwright._impl._video import Video from playwright._impl._waiter import Waiter from playwright._impl._web_mcp import WebMCP diff --git a/playwright/_impl/_protocol_check.py b/playwright/_impl/_protocol_check.py index 04aa5c959..1739381ea 100644 --- a/playwright/_impl/_protocol_check.py +++ b/playwright/_impl/_protocol_check.py @@ -12,14 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Checks each message that the client sends against the protocol spec -# (_protocol.py). The driver drops parameters that it does not know without an -# error, so a wrong parameter name in the client is silent without this check. -# -# The check is on when PW_CHECK_PROTOCOL=1; the test suite sets it. It does not -# raise: it records each violation, and the test suite fails the test that -# caused it. So a violation in a message that is sent without waiting for the -# reply is reported as well. +"""Port of the type checks in packages/protocol/src/validatorPrimitives.ts. + +Checks each message that the client sends against the protocol spec +(_protocol.py). The driver drops parameters that it does not know without an +error, so a wrong parameter name in the client is silent without this check. + +The check is on when PW_CHECK_PROTOCOL=1; the test suite sets it. It does not +raise: it records each violation, and the test suite fails the test that +caused it. So a violation in a message that is sent without waiting for the +reply is reported as well. +""" import os from typing import Any, Callable, Dict, List, Optional diff --git a/playwright/_impl/_serializers.py b/playwright/_impl/_serializers.py new file mode 100644 index 000000000..35e28a499 --- /dev/null +++ b/playwright/_impl/_serializers.py @@ -0,0 +1,260 @@ +# 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. + +"""Port of packages/protocol/src/serializers.ts. + +Converts values to and from the protocol's SerializedValue. It has no asyncio, +greenlet or I/O, so every implementation of the API can use it. +""" + +import base64 +import collections.abc +import datetime +import math +import re +import struct +import traceback +from typing import Any, Callable, Dict, List, Optional, Pattern +from urllib.parse import ParseResult, urlparse, urlunparse + +from playwright._impl._errors import Error +from playwright._impl._map import Map +from playwright._impl._str_utils import escape_regex_flags, parse_regex_flags + +# Returns the serialized form of a handle, for example {"h": 0}, or None if the +# value is not a handle. Upstream passes the same callback to serializeValue(). +HandleSerializer = Callable[[Any], Optional[Dict[str, Any]]] + + +class VisitorInfo: + visited: Map[Any, int] + last_id: int + + def __init__(self) -> None: + self.visited = Map() + self.last_id = 0 + + def visit(self, obj: Any) -> int: + assert obj not in self.visited + self.last_id += 1 + self.visited[obj] = self.last_id + return self.last_id + + +def serialize_value( + value: Any, + handle_serializer: HandleSerializer, + visitor_info: Optional[VisitorInfo] = None, +) -> Any: + if visitor_info is None: + visitor_info = VisitorInfo() + handle = handle_serializer(value) + if handle is not None: + return handle + if value is None: + return dict(v="null") + if isinstance(value, float): + if value == float("inf"): + return dict(v="Infinity") + if value == float("-inf"): + return dict(v="-Infinity") + if value == 0 and math.copysign(1.0, value) == -1.0: + return dict(v="-0") + if math.isnan(value): + return dict(v="NaN") + if isinstance(value, datetime.datetime): + # Node.js Date objects are always in UTC. + return { + "d": datetime.datetime.strftime( + value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ" + ) + } + if isinstance(value, Exception): + return { + "e": { + "m": str(value), + "n": ( + (value.name or "") + if isinstance(value, Error) + else value.__class__.__name__ + ), + "s": ( + (value.stack or "") + if isinstance(value, Error) + else "".join( + traceback.format_exception(type(value), value=value, tb=None) + ) + ), + } + } + if isinstance(value, bool): + return {"b": value} + if isinstance(value, (int, float)): + return {"n": value} + if isinstance(value, str): + return {"s": value} + if isinstance(value, ParseResult): + return {"u": urlunparse(value)} + if isinstance(value, Pattern): + if not isinstance(value.pattern, str): + raise Error("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]) + + if isinstance(value, collections.abc.Sequence) and not isinstance(value, str): + id = visitor_info.visit(value) + a = [] + for e in value: + a.append(serialize_value(e, handle_serializer, visitor_info)) + return dict(a=a, id=id) + + if isinstance(value, dict): + id = visitor_info.visit(value) + o = [] + for name in value: + o.append( + { + "k": name, + "v": serialize_value(value[name], handle_serializer, visitor_info), + } + ) + return dict(o=o, id=id) + return dict(v="undefined") + + +def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: + if refs is None: + refs = {} + if value is None: + return None + if isinstance(value, dict): + if "ref" in value: + return refs[value["ref"]] + + if "v" in value: + v = value["v"] + if v == "Infinity": + return float("inf") + if v == "-Infinity": + return float("-inf") + if v == "-0": + return float("-0") + if v == "NaN": + return float("nan") + if v == "undefined": + return None + if v == "null": + return None + return v + + if "u" in value: + return urlparse(value["u"]) + + if "bi" in value: + return int(value["bi"]) + + if "e" in value: + error = Error(value["e"]["m"]) + error._name = value["e"]["n"] + 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 + for e in value["a"]: + a.append(parse_value(e, refs)) + return a + + if "d" in value: + # Node.js Date objects are always in UTC. + return datetime.datetime.strptime( + value["d"], "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=datetime.timezone.utc) + + if "o" in value: + o: Dict = {} + refs[value["id"]] = o + for e in value["o"]: + o[e["k"]] = parse_value(e["v"], refs) + return o + + if "n" in value: + return value["n"] + + if "s" in value: + return value["s"] + + if "b" in value: + return value["b"] + + if "ta" in value: + encoded_bytes = value["ta"]["b"] + decoded_bytes = base64.b64decode(encoded_bytes) + array_type = value["ta"]["k"] + if array_type == "i8": + word_size = 1 + fmt = "b" + elif array_type == "ui8" or array_type == "ui8c": + word_size = 1 + fmt = "B" + elif array_type == "i16": + word_size = 2 + fmt = "h" + elif array_type == "ui16": + word_size = 2 + fmt = "H" + elif array_type == "i32": + word_size = 4 + fmt = "i" + elif array_type == "ui32": + word_size = 4 + fmt = "I" + elif array_type == "f32": + word_size = 4 + fmt = "f" + elif array_type == "f64": + word_size = 8 + fmt = "d" + elif array_type == "bi64": + word_size = 8 + fmt = "q" + elif array_type == "bui64": + word_size = 8 + fmt = "Q" + else: + raise ValueError(f"Unsupported array type: {array_type}") + + byte_len = len(decoded_bytes) + if byte_len % word_size != 0: + raise ValueError( + f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}" + ) + + if byte_len == 0: + return [] + array_len = byte_len // word_size + # "<" denotes little-endian + format_string = f"<{array_len}{fmt}" + return list(struct.unpack(format_string, decoded_bytes)) + return value diff --git a/playwright/_impl/_set_input_files_helpers.py b/playwright/_impl/_set_input_files_helpers.py index 65307e0a2..0924261b0 100644 --- a/playwright/_impl/_set_input_files_helpers.py +++ b/playwright/_impl/_set_input_files_helpers.py @@ -29,7 +29,7 @@ ) from playwright._impl._connection import Channel, from_channel -from playwright._impl._helper import Error +from playwright._impl._errors import Error from playwright._impl._writable_stream import WritableStream if TYPE_CHECKING: # pragma: no cover diff --git a/playwright/_impl/_sync_base.py b/playwright/_impl/_sync_base.py index 35ddc7302..17726f71d 100644 --- a/playwright/_impl/_sync_base.py +++ b/playwright/_impl/_sync_base.py @@ -30,8 +30,8 @@ ) from playwright._impl._connection import _capture_stack_trace +from playwright._impl._errors import Error from playwright._impl._greenlets import connection_closed_error, wait_for_future -from playwright._impl._helper import Error from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper mapping = ImplToApiMapping() diff --git a/playwright/_impl/_time.py b/playwright/_impl/_time.py new file mode 100644 index 000000000..dbc68a318 --- /dev/null +++ b/playwright/_impl/_time.py @@ -0,0 +1,25 @@ +# 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. + +"""Port of packages/isomorphic/time.ts.""" + +import math +import time + +DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS = 30000 +DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS = 180000 + + +def monotonic_time() -> int: + return math.floor(time.monotonic() * 1000) diff --git a/playwright/_impl/_timeout_settings.py b/playwright/_impl/_timeout_settings.py new file mode 100644 index 000000000..d6a6bc490 --- /dev/null +++ b/playwright/_impl/_timeout_settings.py @@ -0,0 +1,72 @@ +# 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. + +"""Port of packages/playwright-core/src/client/timeoutSettings.ts.""" + +from typing import Optional + +from playwright._impl._time import ( + DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS, + DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS, +) + + +class TimeoutSettings: + + @staticmethod + def launch_timeout(timeout: Optional[float] = None) -> float: + return ( + timeout + if timeout is not None + else DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS + ) + + def __init__(self, parent: Optional["TimeoutSettings"]) -> None: + self._parent = parent + self._default_timeout: Optional[float] = None + self._default_navigation_timeout: Optional[float] = None + + def set_default_timeout(self, timeout: Optional[float]) -> None: + self._default_timeout = timeout + + def timeout(self, timeout: float = None) -> float: + if timeout is not None: + return timeout + if self._default_timeout is not None: + return self._default_timeout + if self._parent: + return self._parent.timeout() + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS + + def set_default_navigation_timeout( + self, navigation_timeout: Optional[float] + ) -> None: + self._default_navigation_timeout = navigation_timeout + + def default_navigation_timeout(self) -> Optional[float]: + return self._default_navigation_timeout + + def default_timeout(self) -> Optional[float]: + return self._default_timeout + + def navigation_timeout(self, timeout: float = None) -> float: + if timeout is not None: + return timeout + if self._default_navigation_timeout is not None: + return self._default_navigation_timeout + if self._default_timeout is not None: + return self._default_timeout + if self._parent: + return self._parent.navigation_timeout() + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index 6821ef09c..0ebd11818 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -23,7 +23,8 @@ from_nullable_channel, ) from playwright._impl._disposable import DisposableStub -from playwright._impl._helper import Error, locals_to_params +from playwright._impl._errors import Error +from playwright._impl._helper import locals_to_params class Tracing(ChannelOwner): diff --git a/playwright/_impl/_url_match.py b/playwright/_impl/_url_match.py index 463e9b701..89a5d891b 100644 --- a/playwright/_impl/_url_match.py +++ b/playwright/_impl/_url_match.py @@ -12,6 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Port of packages/isomorphic/urlMatch.ts. + +Glob and URL matching. It has no asyncio, greenlet or I/O, so every +implementation of the API can use it. +""" + import re from typing import Callable, Dict, Optional, Pattern, Tuple, Union from urllib.parse import ParseResult, urljoin, urlparse, urlunparse diff --git a/playwright/_impl/_video.py b/playwright/_impl/_video.py index 0d64866a7..7da1b8046 100644 --- a/playwright/_impl/_video.py +++ b/playwright/_impl/_video.py @@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Optional, Union from playwright._impl._artifact import Artifact -from playwright._impl._helper import Error +from playwright._impl._errors import Error if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page diff --git a/playwright/_impl/_web_error.py b/playwright/_impl/_web_error.py index 4527e8d4b..af390950e 100644 --- a/playwright/_impl/_web_error.py +++ b/playwright/_impl/_web_error.py @@ -16,7 +16,7 @@ from typing import Any, Optional from playwright._impl._api_structures import WebErrorLocation -from playwright._impl._helper import Error +from playwright._impl._errors import Error from playwright._impl._page import Page