Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ pre-commit run --all-files

For more details look at the [CI configuration](./blob/master/.github/workflows/ci.yml).

Collect coverage

```sh
pytest --browser chromium --cov-report html --cov=playwright
open htmlcov/index.html
```

### Regenerating APIs

```bash
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->95.0.4636.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->96.0.4641.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->15.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->91.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->92.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
11 changes: 10 additions & 1 deletion playwright/_impl/_api_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

import sys
from typing import List, Optional, Union
from typing import Dict, List, Optional, Union

if sys.version_info >= (3, 8): # pragma: no cover
from typing import Literal, TypedDict
Expand Down Expand Up @@ -139,3 +139,12 @@ class SecurityDetails(TypedDict):
subjectName: Optional[str]
validFrom: Optional[float]
validTo: Optional[float]


class NameValue(TypedDict):
name: str
value: str


HeadersArray = List[NameValue]
Headers = Dict[str, str]
23 changes: 6 additions & 17 deletions playwright/_impl/_browser_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
ForcedColors,
ReducedMotion,
locals_to_params,
not_installed_error,
)
from playwright._impl._transport import WebSocketTransport
from playwright._impl._wait_helper import throw_on_timeout
Expand Down Expand Up @@ -86,12 +85,7 @@ async def launch(
) -> Browser:
params = locals_to_params(locals())
normalize_launch_params(params)
try:
return from_channel(await self._channel.send("launch", params))
except Exception as e:
if "npx playwright install" in str(e):
raise not_installed_error(f'"{self.name}" browser was not found.')
raise e
return from_channel(await self._channel.send("launch", params))

async def launch_persistent_context(
self,
Expand Down Expand Up @@ -144,16 +138,11 @@ async def launch_persistent_context(
params = locals_to_params(locals())
await normalize_context_params(self._connection._is_sync, params)
normalize_launch_params(params)
try:
context = from_channel(
await self._channel.send("launchPersistentContext", params)
)
context._options = params
return context
except Exception as e:
if "npx playwright install" in str(e):
raise not_installed_error(f'"{self.name}" browser was not found.')
raise e
context = from_channel(
await self._channel.send("launchPersistentContext", params)
)
context._options = params
return context

async def connect_over_cdp(
self,
Expand Down
22 changes: 2 additions & 20 deletions playwright/_impl/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
)
from urllib.parse import urljoin

from playwright._impl._api_structures import NameValue
from playwright._impl._api_types import Error, TimeoutError

if sys.version_info >= (3, 8): # pragma: no cover
Expand Down Expand Up @@ -69,15 +70,10 @@ class ErrorPayload(TypedDict, total=False):
value: Any


class Header(TypedDict):
name: str
value: str


class ContinueParameters(TypedDict, total=False):
url: Optional[str]
method: Optional[str]
headers: Optional[List[Header]]
headers: Optional[List[NameValue]]
postData: Optional[str]


Expand Down Expand Up @@ -234,20 +230,6 @@ def is_safe_close_error(error: Exception) -> bool:
)


def not_installed_error(message: str) -> Exception:
return Error(
f"""
================================================================================
{message}
Please complete Playwright installation via running

"python -m playwright install"

================================================================================
"""
)


to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))")


Expand Down
3 changes: 3 additions & 0 deletions playwright/_impl/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ async def dblclick(
) -> None:
await self.click(x, y, delay=delay, button=button, clickCount=2)

async def wheel(self, deltaX: float, deltaY: float) -> None:
await self._channel.send("mouseWheel", locals_to_params(locals()))


class Touchscreen:
def __init__(self, channel: Channel) -> None:
Expand Down
106 changes: 64 additions & 42 deletions playwright/_impl/_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
import base64
import json
import mimetypes
from collections import defaultdict
from pathlib import Path
from types import SimpleNamespace
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, cast
from urllib import parse

from playwright._impl._api_structures import (
Headers,
HeadersArray,
RemoteAddr,
RequestSizes,
ResourceTiming,
Expand All @@ -34,7 +37,7 @@
from_nullable_channel,
)
from playwright._impl._event_context_manager import EventContextManagerImpl
from playwright._impl._helper import ContinueParameters, Header, locals_to_params
from playwright._impl._helper import ContinueParameters, locals_to_params
from playwright._impl._wait_helper import WaitHelper

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -64,8 +67,8 @@ def __init__(
"responseStart": -1,
"responseEnd": -1,
}
self._headers: List[Header] = self._initializer["headers"]
self._all_headers_future: Optional[asyncio.Future[List[Header]]] = None
self._provisional_headers = RawHeaders(self._initializer["headers"])
self._all_headers_future: Optional[asyncio.Future[RawHeaders]] = None

def __repr__(self) -> str:
return f"<Request url={self.url!r} method={self.method!r}>"
Expand Down Expand Up @@ -115,10 +118,6 @@ def post_data_buffer(self) -> Optional[bytes]:
return None
return base64.b64decode(b64_content)

@property
def headers(self) -> Dict[str, str]:
return headers_array_to_object(self._headers, True)

async def response(self) -> Optional["Response"]:
return from_nullable_channel(await self._channel.send("response"))

Expand All @@ -145,25 +144,27 @@ def failure(self) -> Optional[str]:
def timing(self) -> ResourceTiming:
return self._timing

async def all_headers(self) -> Dict[str, str]:
return headers_array_to_object(await self._get_headers_if_needed(), True)
@property
def headers(self) -> Headers:
return self._provisional_headers.headers()

async def headers_array(self) -> List[List[str]]:
return list(
map(
lambda header: [header["name"], header["value"]],
await self._get_headers_if_needed(),
)
)
async def all_headers(self) -> Headers:
return (await self._actual_headers()).headers()

async def headers_array(self) -> HeadersArray:
return (await self._actual_headers()).headers_array()

async def header_value(self, name: str) -> Optional[str]:
return (await self._actual_headers()).get(name)

async def _get_headers_if_needed(self) -> List[Header]:
async def _actual_headers(self) -> "RawHeaders":
if not self._all_headers_future:
self._all_headers_future = asyncio.Future()
response = await self.response()
if not response:
return self._headers
return self._provisional_headers
headers = await response._channel.send("rawRequestHeaders")
self._all_headers_future.set_result(headers)
self._all_headers_future.set_result(RawHeaders(headers))
return await self._all_headers_future


Expand Down Expand Up @@ -256,10 +257,10 @@ def __init__(
self._request._timing["connectEnd"] = timing["connectEnd"]
self._request._timing["requestStart"] = timing["requestStart"]
self._request._timing["responseStart"] = timing["responseStart"]
self._headers = headers_array_to_object(
cast(List[Header], self._initializer["headers"]), True
self._provisional_headers = RawHeaders(
cast(HeadersArray, self._initializer["headers"])
)
self._raw_headers_future: Optional[asyncio.Future[List[Header]]] = None
self._raw_headers_future: Optional[asyncio.Future[RawHeaders]] = None
self._finished_future: asyncio.Future[bool] = asyncio.Future()

def __repr__(self) -> str:
Expand All @@ -284,25 +285,26 @@ def status_text(self) -> str:
return self._initializer["statusText"]

@property
def headers(self) -> Dict[str, str]:
return self._headers.copy()
def headers(self) -> Headers:
return self._provisional_headers.headers()

async def all_headers(self) -> Dict[str, str]:
return headers_array_to_object(await self._get_headers_if_needed(), True)
async def all_headers(self) -> Headers:
return (await self._actual_headers()).headers()

async def headers_array(self) -> List[List[str]]:
return list(
map(
lambda header: [header["name"], header["value"]],
await self._get_headers_if_needed(),
)
)
async def headers_array(self) -> HeadersArray:
return (await self._actual_headers()).headers_array()

async def header_value(self, name: str) -> Optional[str]:
return (await self._actual_headers()).get(name)

async def header_values(self, name: str) -> List[str]:
return (await self._actual_headers()).get_all(name)

async def _get_headers_if_needed(self) -> List[Header]:
async def _actual_headers(self) -> "RawHeaders":
if not self._raw_headers_future:
self._raw_headers_future = asyncio.Future()
headers = cast(List[Header], await self._channel.send("rawResponseHeaders"))
self._raw_headers_future.set_result(headers)
headers = cast(HeadersArray, await self._channel.send("rawResponseHeaders"))
self._raw_headers_future.set_result(RawHeaders(headers))
return await self._raw_headers_future

async def server_addr(self) -> Optional[RemoteAddr]:
Expand Down Expand Up @@ -420,12 +422,32 @@ def _on_close(self) -> None:
self.emit(WebSocket.Events.Close)


def serialize_headers(headers: Dict[str, str]) -> List[Header]:
def serialize_headers(headers: Dict[str, str]) -> HeadersArray:
return [{"name": name, "value": value} for name, value in headers.items()]


def headers_array_to_object(headers: List[Header], lower_case: bool) -> Dict[str, str]:
return {
(header["name"].lower() if lower_case else header["name"]): header["value"]
for header in headers
}
class RawHeaders:
def __init__(self, headers: HeadersArray) -> None:
self._headers_array = headers
self._headers_map: Dict[str, Dict[str, bool]] = defaultdict(dict)
for header in headers:
self._headers_map[header["name"].lower()][header["value"]] = True

def get(self, name: str) -> Optional[str]:
values = self.get_all(name)
if not values:
return None
separator = "\n" if name.lower() == "set-cookie" else ", "
return separator.join(values)

def get_all(self, name: str) -> List[str]:
return list(self._headers_map[name.lower()].keys())

def headers(self) -> Dict[str, str]:
result = {}
for name in self._headers_map.keys():
result[name] = cast(str, self.get(name))
return result

def headers_array(self) -> HeadersArray:
return self._headers_array
Loading