FAEA/venv/lib/python3.10/site-packages/playwright/_impl/_helper.py
2025-12-22 17:14:46 +08:00

391 lines
11 KiB
Python

# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import fnmatch
import inspect
import math
import os
import re
import sys
import time
import traceback
from pathlib import Path
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
Callable,
Coroutine,
Dict,
List,
Optional,
Pattern,
TypeVar,
Union,
cast,
)
from urllib.parse import urljoin
from greenlet import greenlet
from playwright._impl._api_structures import NameValue
from playwright._impl._errors import Error, TargetClosedError, TimeoutError
from playwright._impl._str_utils import escape_regex_flags
if sys.version_info >= (3, 8): # pragma: no cover
from typing import Literal, TypedDict
else: # pragma: no cover
from typing_extensions import Literal, TypedDict
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._api_structures import HeadersArray
from playwright._impl._network import Request, Response, Route
URLMatch = Union[str, Pattern[str], Callable[[str], bool]]
URLMatchRequest = Union[str, Pattern[str], Callable[["Request"], bool]]
URLMatchResponse = Union[str, Pattern[str], Callable[["Response"], bool]]
RouteHandlerCallback = Union[
Callable[["Route"], Any], Callable[["Route", "Request"], Any]
]
ColorScheme = Literal["dark", "light", "no-preference", "null"]
ForcedColors = Literal["active", "none", "null"]
ReducedMotion = Literal["no-preference", "null", "reduce"]
DocumentLoadState = Literal["commit", "domcontentloaded", "load", "networkidle"]
KeyboardModifier = Literal["Alt", "Control", "Meta", "Shift"]
MouseButton = Literal["left", "middle", "right"]
ServiceWorkersPolicy = Literal["allow", "block"]
HarMode = Literal["full", "minimal"]
HarContentPolicy = Literal["attach", "embed", "omit"]
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]
def prepare_record_har_options(params: Dict) -> Dict[str, Any]:
out_params: Dict[str, Any] = {"path": str(params["recordHarPath"])}
if "recordHarUrlFilter" in params:
opt = params["recordHarUrlFilter"]
if isinstance(opt, str):
out_params["urlGlob"] = opt
if isinstance(opt, Pattern):
out_params["urlRegexSource"] = opt.pattern
out_params["urlRegexFlags"] = escape_regex_flags(opt)
del params["recordHarUrlFilter"]
if "recordHarMode" in params:
out_params["mode"] = params["recordHarMode"]
del params["recordHarMode"]
new_content_api = None
old_content_api = None
if "recordHarContent" in params:
new_content_api = params["recordHarContent"]
del params["recordHarContent"]
if "recordHarOmitContent" in params:
old_content_api = params["recordHarOmitContent"]
del params["recordHarOmitContent"]
content = new_content_api or ("omit" if old_content_api else None)
if content:
out_params["content"] = content
return out_params
class ParsedMessageParams(TypedDict):
type: str
guid: str
initializer: Dict
class ParsedMessagePayload(TypedDict, total=False):
id: int
guid: str
method: str
params: ParsedMessageParams
result: Any
error: ErrorPayload
class Document(TypedDict):
request: Optional[Any]
class FrameNavigatedEvent(TypedDict):
url: str
name: str
newDocument: Optional[Document]
error: Optional[str]
Env = Dict[str, Union[str, float, bool]]
class URLMatcher:
def __init__(self, base_url: Union[str, None], match: URLMatch) -> None:
self._callback: Optional[Callable[[str], bool]] = None
self._regex_obj: Optional[Pattern[str]] = None
if isinstance(match, str):
if base_url and not match.startswith("*"):
match = urljoin(base_url, match)
regex = fnmatch.translate(match)
self._regex_obj = re.compile(regex)
elif isinstance(match, Pattern):
self._regex_obj = match
else:
self._callback = match
self.match = match
def matches(self, url: str) -> bool:
if self._callback:
return self._callback(url)
if self._regex_obj:
return cast(bool, self._regex_obj.search(url))
return False
class HarLookupResult(TypedDict, total=False):
action: Literal["error", "redirect", "fulfill", "noentry"]
message: Optional[str]
redirectURL: Optional[str]
status: Optional[int]
headers: Optional["HeadersArray"]
body: Optional[str]
class TimeoutSettings:
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 30000
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) -> float:
if self._default_navigation_timeout is not None:
return self._default_navigation_timeout
if self._parent:
return self._parent.navigation_timeout()
return 30000
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) -> Error:
base_error_class = Error
if error.get("name") == "TimeoutError":
base_error_class = TimeoutError
if error.get("name") == "TargetClosedError":
base_error_class = TargetClosedError
exc = base_error_class(cast(str, patch_error_message(error.get("message"))))
exc._name = error["name"]
exc._stack = error["stack"]
return exc
def patch_error_message(message: Optional[str]) -> Optional[str]:
if message is None:
return None
match = re.match(r"(\w+)(: expected .*)", message)
if match:
message = to_snake_case(match.group(1)) + match.group(2)
assert message is not None
message = message.replace(
"Pass { acceptDownloads: true }", "Pass { accept_downloads: True }"
)
return message
def locals_to_params(args: Dict) -> Dict:
copy = {}
for key in args:
if key == "self":
continue
if args[key] is not None:
copy[key] = args[key]
return copy
def monotonic_time() -> int:
return math.floor(time.monotonic() * 1000)
class RouteHandler:
def __init__(
self,
matcher: URLMatcher,
handler: RouteHandlerCallback,
is_sync: bool,
times: Optional[int] = None,
):
self.matcher = matcher
self.handler = handler
self._times = times if times else math.inf
self._handled_count = 0
self._is_sync = is_sync
def matches(self, request_url: str) -> bool:
return self.matcher.matches(request_url)
async def handle(self, route: "Route") -> bool:
handled_future = route._start_handling()
handler_task = []
def impl() -> None:
self._handled_count += 1
result = cast(
Callable[["Route", "Request"], Union[Coroutine, Any]], self.handler
)(route, route.request)
if inspect.iscoroutine(result):
handler_task.append(asyncio.create_task(result))
# As with event handlers, each route handler is a potentially blocking context
# so it needs a fiber.
if self._is_sync:
g = greenlet(impl)
g.switch()
else:
impl()
[handled, *_] = await asyncio.gather(handled_future, *handler_task)
return handled
@property
def will_expire(self) -> bool:
return self._handled_count + 1 >= self._times
@staticmethod
def prepare_interception_patterns(
handlers: List["RouteHandler"],
) -> List[Dict[str, str]]:
patterns = []
all = False
for handler in handlers:
if isinstance(handler.matcher.match, str):
patterns.append({"glob": handler.matcher.match})
elif isinstance(handler.matcher._regex_obj, re.Pattern):
patterns.append(
{
"regexSource": handler.matcher._regex_obj.pattern,
"regexFlags": escape_regex_flags(handler.matcher._regex_obj),
}
)
else:
all = True
if all:
return [{"glob": "**/*"}]
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
os.makedirs(os.path.dirname(path), exist_ok=True)
async def async_writefile(file: Union[str, Path], data: Union[str, bytes]) -> None:
def inner() -> None:
with open(file, "w" if isinstance(data, str) else "wb") as fh:
fh.write(data)
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, inner)
async def async_readfile(file: Union[str, Path]) -> bytes:
def inner() -> bytes:
with open(file, "rb") as fh:
return fh.read()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, inner)
T = TypeVar("T")
def to_impl(obj: T) -> T:
if hasattr(obj, "_impl_obj"):
return cast(Any, obj)._impl_obj
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)
and "name" in value
and "mimeType" in value
and "buffer" in value
)
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))