diff --git a/py/selenium/webdriver/common/api_request_context.py b/py/selenium/webdriver/common/api_request_context.py index 9c3a0129d16cf..bf23cd25a4671 100644 --- a/py/selenium/webdriver/common/api_request_context.py +++ b/py/selenium/webdriver/common/api_request_context.py @@ -29,6 +29,8 @@ import urllib3 from urllib3.util.retry import Retry +from selenium.webdriver.common.cookie import Cookie + if TYPE_CHECKING: from selenium.webdriver.remote.webdriver import WebDriver @@ -98,7 +100,7 @@ def dispose(self) -> None: self._body = b"" -def _cookie_matches(cookie: dict, url: str, default_domain: str = "") -> bool: +def _cookie_matches(cookie: Cookie, url: str, default_domain: str = "") -> bool: """Check if a browser cookie should be sent with a request to the given URL. Evaluates expiry, domain, path, and secure attribute matching per RFC 6265. @@ -151,7 +153,7 @@ def _cookie_matches(cookie: dict, url: str, default_domain: str = "") -> bool: return True -def _parse_set_cookie(header_value: str) -> dict: +def _parse_set_cookie(header_value: str) -> Cookie: """Parse a single Set-Cookie header value into a cookie dict. Uses manual parsing instead of http.cookies.SimpleCookie which is too @@ -171,7 +173,7 @@ def _parse_set_cookie(header_value: str) -> dict: name = name_value[:eq_idx].strip() value = name_value[eq_idx + 1 :].strip() - cookie: dict[str, Any] = {"name": name, "value": value} + cookie: Cookie = {"name": name, "value": value} has_max_age = False for part in parts[1:]: @@ -455,7 +457,7 @@ def _build_response(self, resp: urllib3.BaseHTTPResponse, url: str) -> APIRespon body=resp.data, ) - def _get_cookies_for_request(self, url: str) -> list[dict]: + def _get_cookies_for_request(self, url: str) -> list[Cookie]: """Get cookies that should be sent with the request. Overridden by subclasses.""" return [] @@ -558,7 +560,7 @@ def new_context( Returns: An _IsolatedAPIRequestContext instance. """ - cookies: list[dict] = [] + cookies: list[Cookie] = [] if storage_state is not None: if isinstance(storage_state, (str, pathlib.Path)): file_path = pathlib.Path(storage_state) @@ -604,7 +606,7 @@ def get_storage_state(self, path: str | pathlib.Path | None = None) -> dict[str, raise OSError(f"Cannot write storage state to {file_path}: {e}") from e return state - def _get_cookies_for_request(self, url: str) -> list[dict]: + def _get_cookies_for_request(self, url: str) -> list[Cookie]: """Get matching browser cookies for the request URL.""" try: browser_cookies = self._driver.get_cookies() @@ -657,7 +659,7 @@ def __init__( self, base_url: str = "", extra_headers: dict[str, str] | None = None, - cookies: list[dict] | None = None, + cookies: list[Cookie] | None = None, timeout: float = 30.0, max_redirects: int = 10, fail_on_status_code: bool = False, @@ -669,13 +671,13 @@ def __init__( max_redirects=max_redirects, fail_on_status_code=fail_on_status_code, ) - self._cookies: list[dict] = cookies or [] + self._cookies: list[Cookie] = cookies or [] def get_storage_state(self) -> dict[str, Any]: """Return the current cookies as a storage state dict.""" return {"cookies": list(self._cookies)} - def _get_cookies_for_request(self, url: str) -> list[dict]: + def _get_cookies_for_request(self, url: str) -> list[Cookie]: """Get matching cookies from the internal jar.""" # For isolated contexts, use the request hostname as default domain default_domain = urllib.parse.urlparse(url).hostname or "" diff --git a/py/selenium/webdriver/common/cookie.py b/py/selenium/webdriver/common/cookie.py new file mode 100644 index 0000000000000..7f6c6a040d6d0 --- /dev/null +++ b/py/selenium/webdriver/common/cookie.py @@ -0,0 +1,31 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing_extensions import NotRequired, TypedDict + + +class Cookie(TypedDict, total=False): + """A WebDriver cookie dictionary.""" + + name: str + value: str + path: NotRequired[str] + domain: NotRequired[str] + secure: NotRequired[bool] + httpOnly: NotRequired[bool] + expiry: NotRequired[int] + sameSite: NotRequired[str] diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index c8177068efd1b..f385ebac45666 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -55,6 +55,7 @@ from selenium.webdriver.common.bidi.storage import Storage from selenium.webdriver.common.bidi.webextension import WebExtension from selenium.webdriver.common.by import By +from selenium.webdriver.common.cookie import Cookie from selenium.webdriver.common.fedcm.dialog import Dialog from selenium.webdriver.common.options import ArgOptions, BaseOptions from selenium.webdriver.common.print_page_options import PrintOptions @@ -689,7 +690,7 @@ def refresh(self) -> None: """Refreshes the current page.""" self.execute(Command.REFRESH) - def get_cookies(self) -> list[dict]: + def get_cookies(self) -> list[Cookie]: """Get all cookies visible to the current WebDriver instance. Returns: @@ -698,7 +699,7 @@ def get_cookies(self) -> list[dict]: """ return self.execute(Command.GET_ALL_COOKIES)["value"] - def get_cookie(self, name) -> dict | None: + def get_cookie(self, name: str) -> Cookie | None: """Get a single cookie by name (case-sensitive,). Returns: @@ -737,7 +738,7 @@ def delete_all_cookies(self) -> None: """Delete all cookies in the scope of the session.""" self.execute(Command.DELETE_ALL_COOKIES) - def add_cookie(self, cookie_dict) -> None: + def add_cookie(self, cookie_dict: Cookie) -> None: """Adds a cookie to your current session. Args: