Skip to content
Open
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
3 changes: 2 additions & 1 deletion pydoll/browser/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod

from pydoll.browser.preference_types import BrowserPreferences
from pydoll.constants import PageLoadState


Expand All @@ -25,7 +26,7 @@ def add_argument(self, argument: str):

@property
@abstractmethod
def browser_preferences(self) -> dict:
def browser_preferences(self) -> BrowserPreferences:
pass

@property
Expand Down
4 changes: 2 additions & 2 deletions pydoll/browser/managers/temp_dir_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ def handle_cleanup_error(self, func: Callable[[str], None], path: str, exc_info:
Note:
Handles Chromium-specific locked files like CrashpadMetrics.
"""
matches = ['CrashpadMetrics-active.pma']
matches = ['CrashpadMetrics-active.pma', 'Cookies', 'Network']
exc_type, exc_value, _ = exc_info

if exc_type is PermissionError:
if Path(path).name in matches:
if Path(path).name in matches or 'Network' in str(Path(path)):
try:
self.retry_process_file(func, path)
return
Expand Down
110 changes: 89 additions & 21 deletions pydoll/browser/options.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from contextlib import suppress
from typing import Any, Optional, cast

from pydoll.browser.interfaces import Options
from pydoll.browser.preference_types import PREFERENCE_SCHEMA, BrowserPreferences
from pydoll.constants import PageLoadState
from pydoll.exceptions import (
ArgumentAlreadyExistsInOptions,
ArgumentNotFoundInOptions,
InvalidPreferencePath,
InvalidPreferenceValue,
WrongPrefsDict,
)

Expand All @@ -24,12 +28,12 @@ def __init__(self):
Sets up an empty list for command-line arguments and a string
for the binary location of the browser.
"""
self._arguments = []
self._binary_location = ''
self._start_timeout = 10
self._browser_preferences = {}
self._headless = False
self._page_load_state = PageLoadState.COMPLETE
self._arguments: list[str] = []
self._binary_location: str = ''
self._start_timeout: int = 10
self._browser_preferences: dict[str, Any] = {}
self._headless: bool = False
self._page_load_state: PageLoadState = PageLoadState.COMPLETE

@property
def arguments(self) -> list[str]:
Expand Down Expand Up @@ -121,16 +125,18 @@ def remove_argument(self, argument: str):
self._arguments.remove(argument)

@property
def browser_preferences(self) -> dict:
return self._browser_preferences
def browser_preferences(self) -> BrowserPreferences:
return cast(BrowserPreferences, self._browser_preferences)

@browser_preferences.setter
def browser_preferences(self, preferences: dict):
def browser_preferences(self, preferences: BrowserPreferences):
if not isinstance(preferences, dict):
raise ValueError('The experimental options value must be a dict.')

if preferences.get('prefs'):
raise WrongPrefsDict
# deixar o WrongPrefsDict, mas com mensagem para ficar menos genérico
raise WrongPrefsDict("Top-level key 'prefs' is not allowed in browser preferences.")
# merge com preferências existentes
self._browser_preferences = {**self._browser_preferences, **preferences}

def _set_pref_path(self, path: list, value):
Expand All @@ -143,11 +149,65 @@ def _set_pref_path(self, path: list, value):
path (e.g., ['plugins', 'always_open_pdf_externally'])
value -- The value to set at the given path
"""
d = self._browser_preferences
# validation will be handled in the updated implementation below
# (kept for backward-compatibility if callers rely on signature)
self._validate_pref_path(path)
self._validate_pref_value(path, value)

d = cast(dict[str, Any], self._browser_preferences)
for key in path[:-1]:
d = d.setdefault(key, {})
d[path[-1]] = value

@staticmethod
def _validate_pref_path(path: list[str]) -> None:
"""
Validate that the provided path exists in the PREFERENCE_SCHEMA.
Raises InvalidPreferencePath when any segment is invalid.
"""
node = PREFERENCE_SCHEMA
for key in path:
if isinstance(node, dict) and key in node:
node = node[key]
else:
raise InvalidPreferencePath(f'Invalid preference path: {".".join(path)}')

@staticmethod
def _validate_pref_value(path: list[str], value: Any) -> None:
"""
Validate the value type for the final segment in path against PREFERENCE_SCHEMA.
Supports recursive validation for nested dictionaries.
Raises InvalidPreferenceValue or InvalidPreferencePath on validation failure.
"""
node = PREFERENCE_SCHEMA
# Walk to the parent node (assumes path is valid from _validate_pref_path)
for key in path[:-1]:
node = node[key]

final_key = path[-1]
expected = node[final_key]

if isinstance(expected, dict):
# Expected is a subschema dict; value must be a dict and match the schema
if not isinstance(value, dict):
raise InvalidPreferenceValue(
f'Invalid value type for {".".join(path)}: '
f'expected dict, got {type(value).__name__}'
)
# Recursively validate each key-value in the value dict
for k, v in value.items():
if k not in expected:
raise InvalidPreferencePath(
f'Invalid key "{k}" in preference path {".".join(path)}'
)
ChromiumOptions._validate_pref_value(path + [k], v)
elif not isinstance(value, expected):
# Expected is a primitive type; check isinstance
raise InvalidPreferenceValue(
f'Invalid value type for {".".join(path)}: '
f'expected {expected.__name__}, got {type(value).__name__}'
)

def _get_pref_path(self, path: list):
"""
Safely gets a nested value from self._browser_preferences.
Expand All @@ -159,6 +219,12 @@ def _get_pref_path(self, path: list):
Returns:
The value at the given path, or None if path doesn't exist
"""
# validate path structure first; if invalid, raise a clear exception
try:
self._validate_pref_path(path)
except InvalidPreferencePath:
raise

nested_preferences = self._browser_preferences
with suppress(KeyError, TypeError):
for key in path:
Expand Down Expand Up @@ -189,8 +255,9 @@ def set_accept_languages(self, languages: str):
self._set_pref_path(['intl', 'accept_languages'], languages)

@property
def prompt_for_download(self) -> bool:
return self._get_pref_path(['download', 'prompt_for_download'])
def prompt_for_download(self) -> Optional[bool]:
val = self._get_pref_path(['download', 'prompt_for_download'])
return val if isinstance(val, bool) else None

@prompt_for_download.setter
def prompt_for_download(self, enabled: bool):
Expand Down Expand Up @@ -223,8 +290,9 @@ def block_popups(self, block: bool):
)

@property
def password_manager_enabled(self) -> bool:
return self._get_pref_path(['profile', 'password_manager_enabled'])
def password_manager_enabled(self) -> Optional[bool]:
val = self._get_pref_path(['profile', 'password_manager_enabled'])
return val if isinstance(val, bool) else None

@password_manager_enabled.setter
def password_manager_enabled(self, enabled: bool):
Expand All @@ -237,7 +305,7 @@ def password_manager_enabled(self, enabled: bool):
enabled: If True, the password manager is active.
"""
self._set_pref_path(['profile', 'password_manager_enabled'], enabled)
self._set_pref_path(['credentials_enable_service'], enabled)
self._browser_preferences['credentials_enable_service'] = enabled

@property
def block_notifications(self) -> bool:
Expand Down Expand Up @@ -291,8 +359,9 @@ def allow_automatic_downloads(self, allow: bool):
)

@property
def open_pdf_externally(self) -> bool:
return self._get_pref_path(['plugins', 'always_open_pdf_externally'])
def open_pdf_externally(self) -> Optional[bool]:
val = self._get_pref_path(['plugins', 'always_open_pdf_externally'])
return val if isinstance(val, bool) else None

@open_pdf_externally.setter
def open_pdf_externally(self, enabled: bool):
Expand All @@ -315,9 +384,8 @@ def headless(self, headless: bool):
self._headless = headless
has_argument = '--headless' in self.arguments
methods_map = {True: self.add_argument, False: self.remove_argument}
if headless == has_argument:
return
methods_map[headless]('--headless')
if headless != has_argument:
methods_map[headless]('--headless')

@property
def page_load_state(self) -> PageLoadState:
Expand Down
56 changes: 56 additions & 0 deletions pydoll/browser/preference_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import TypedDict

from typing_extensions import NotRequired


class DownloadPreferences(TypedDict):
default_directory: str
prompt_for_download: NotRequired[bool]
directory_upgrade: NotRequired[bool]


class ContentSettingValues(TypedDict, total=False):
popups: int
notifications: int
automatic_downloads: int


class ProfilePreferences(TypedDict):
password_manager_enabled: bool
default_content_setting_values: ContentSettingValues


class BrowserPreferences(TypedDict, total=False):
download: DownloadPreferences
profile: ProfilePreferences
intl: NotRequired[dict[str, str]]
plugins: NotRequired[dict[str, bool]]
credentials_enable_service: bool


# Runtime schema used for validating preference paths and value types.
# Keys map to either a python type (str/bool/int/dict) or to a nested dict
# describing child keys and their expected types.
PREFERENCE_SCHEMA: dict = {
'download': {
'default_directory': str,
'prompt_for_download': bool,
'directory_upgrade': bool,
},
'profile': {
'password_manager_enabled': bool,
# default_content_setting_values is a mapping of content name -> int
'default_content_setting_values': {
'popups': int,
'notifications': int,
'automatic_downloads': int,
},
},
'intl': {
'accept_languages': str,
},
'plugins': {
'always_open_pdf_externally': bool,
},
'credentials_enable_service': bool,
}
12 changes: 12 additions & 0 deletions pydoll/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,18 @@ class WrongPrefsDict(PydollException):
message = 'The dict can not contain "prefs" key, provide only the prefs options'


class InvalidPreferencePath(PydollException):
"""Raised when a provided preference path is invalid (segment doesn't exist)."""

message = 'Invalid preference path'


class InvalidPreferenceValue(PydollException):
"""Invalid value for a preference (incompatible type)"""

message = 'Invalid preference value'


class ElementPreconditionError(ElementException):
"""Raised when invalid or missing preconditions are provided for element operations."""

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ pythonpath = "."
addopts = '-p no:warnings'

[tool.taskipy.tasks]
lint = 'ruff check .; ruff check . --diff'
format = 'ruff check . --fix; ruff format .'
lint = 'ruff check . && ruff check . --diff'
format = 'ruff check . --fix && ruff format .'
test = 'pytest -s -x --cov=pydoll -vv'
post_test = 'coverage html'

Expand Down
Loading
Loading