Skip to content

Commit 9b3cc1d

Browse files
refactor: implement browser preferences types and validation
1 parent 7328ae2 commit 9b3cc1d

File tree

4 files changed

+133
-12
lines changed

4 files changed

+133
-12
lines changed

pydoll/browser/options.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from contextlib import suppress
2+
from typing import Any, Optional
23

34
from pydoll.browser.interfaces import Options
5+
from pydoll.browser.preference_types import PREFERENCE_SCHEMA, BrowserPreferences
46
from pydoll.constants import PageLoadState
57
from pydoll.exceptions import (
68
ArgumentAlreadyExistsInOptions,
79
ArgumentNotFoundInOptions,
10+
InvalidPreferencePath,
11+
InvalidPreferenceValue,
812
WrongPrefsDict,
913
)
1014

@@ -27,7 +31,7 @@ def __init__(self):
2731
self._arguments = []
2832
self._binary_location = ''
2933
self._start_timeout = 10
30-
self._browser_preferences = {}
34+
self._browser_preferences: BrowserPreferences = {}
3135
self._headless = False
3236
self._page_load_state = PageLoadState.COMPLETE
3337

@@ -121,16 +125,18 @@ def remove_argument(self, argument: str):
121125
self._arguments.remove(argument)
122126

123127
@property
124-
def browser_preferences(self) -> dict:
128+
def browser_preferences(self) -> BrowserPreferences:
125129
return self._browser_preferences
126130

127131
@browser_preferences.setter
128-
def browser_preferences(self, preferences: dict):
132+
def browser_preferences(self, preferences: BrowserPreferences):
129133
if not isinstance(preferences, dict):
130134
raise ValueError('The experimental options value must be a dict.')
131135

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

136142
def _set_pref_path(self, path: list, value):
@@ -143,11 +149,57 @@ def _set_pref_path(self, path: list, value):
143149
path (e.g., ['plugins', 'always_open_pdf_externally'])
144150
value -- The value to set at the given path
145151
"""
152+
# validation will be handled in the updated implementation below
153+
# (kept for backward-compatibility if callers rely on signature)
154+
self._validate_pref_path(path)
155+
self._validate_pref_value(path, value)
156+
146157
d = self._browser_preferences
147158
for key in path[:-1]:
148159
d = d.setdefault(key, {})
149160
d[path[-1]] = value
150161

162+
@staticmethod
163+
def _validate_pref_path(path: list[str]) -> None:
164+
"""
165+
Validate that the provided path exists in the PREFERENCE_SCHEMA.
166+
Raises InvalidPreferencePath when any segment is invalid.
167+
"""
168+
node = PREFERENCE_SCHEMA
169+
for key in path:
170+
if isinstance(node, dict) and key in node:
171+
node = node[key]
172+
else:
173+
raise InvalidPreferencePath(f'Invalid preference path: {".".join(path)}')
174+
175+
@staticmethod
176+
def _validate_pref_value(path: list[str], value: Any) -> None:
177+
"""
178+
Validate the value type for the final segment in path against PREFERENCE_SCHEMA.
179+
Raises InvalidPreferenceValue when the value does not match expected type.
180+
"""
181+
node = PREFERENCE_SCHEMA
182+
# walk to the parent node
183+
for key in path[:-1]:
184+
node = node[key]
185+
186+
final_key = path[-1]
187+
expected = node.get(final_key) if isinstance(node, dict) else None
188+
189+
if expected is None:
190+
# no explicit restriction
191+
return
192+
193+
if expected is dict:
194+
if not isinstance(value, dict):
195+
msg = f'Invalid value type for {".".join(path)}: '
196+
msg += f'expected dict, got {type(value).__name__}'
197+
raise InvalidPreferenceValue(msg)
198+
elif not isinstance(value, expected):
199+
msg = f'Invalid value type for {".".join(path)}: '
200+
msg += f'expected {expected.__name__}, got {type(value).__name__}'
201+
raise InvalidPreferenceValue(msg)
202+
151203
def _get_pref_path(self, path: list):
152204
"""
153205
Safely gets a nested value from self._browser_preferences.
@@ -159,6 +211,12 @@ def _get_pref_path(self, path: list):
159211
Returns:
160212
The value at the given path, or None if path doesn't exist
161213
"""
214+
# validate path structure first; if invalid, raise a clear exception
215+
try:
216+
self._validate_pref_path(path)
217+
except InvalidPreferencePath:
218+
raise
219+
162220
nested_preferences = self._browser_preferences
163221
with suppress(KeyError, TypeError):
164222
for key in path:
@@ -189,8 +247,9 @@ def set_accept_languages(self, languages: str):
189247
self._set_pref_path(['intl', 'accept_languages'], languages)
190248

191249
@property
192-
def prompt_for_download(self) -> bool:
193-
return self._get_pref_path(['download', 'prompt_for_download'])
250+
def prompt_for_download(self) -> Optional[bool]:
251+
val = self._get_pref_path(['download', 'prompt_for_download'])
252+
return val if isinstance(val, bool) else None
194253

195254
@prompt_for_download.setter
196255
def prompt_for_download(self, enabled: bool):
@@ -223,8 +282,9 @@ def block_popups(self, block: bool):
223282
)
224283

225284
@property
226-
def password_manager_enabled(self) -> bool:
227-
return self._get_pref_path(['profile', 'password_manager_enabled'])
285+
def password_manager_enabled(self) -> Optional[bool]:
286+
val = self._get_pref_path(['profile', 'password_manager_enabled'])
287+
return val if isinstance(val, bool) else None
228288

229289
@password_manager_enabled.setter
230290
def password_manager_enabled(self, enabled: bool):
@@ -291,8 +351,9 @@ def allow_automatic_downloads(self, allow: bool):
291351
)
292352

293353
@property
294-
def open_pdf_externally(self) -> bool:
295-
return self._get_pref_path(['plugins', 'always_open_pdf_externally'])
354+
def open_pdf_externally(self) -> Optional[bool]:
355+
val = self._get_pref_path(['plugins', 'always_open_pdf_externally'])
356+
return val if isinstance(val, bool) else None
296357

297358
@open_pdf_externally.setter
298359
def open_pdf_externally(self, enabled: bool):

pydoll/browser/preference_types.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from typing import NotRequired, TypedDict
2+
3+
4+
class DownloadPreferences(TypedDict, total=False):
5+
default_directory: str
6+
prompt_for_download: bool
7+
8+
9+
class ProfilePreferences(TypedDict, total=False):
10+
password_manager_enabled: bool
11+
# maps content setting name -> int (e.g. popups: 0 or 1)
12+
default_content_setting_values: NotRequired[dict[str, int]]
13+
14+
15+
class BrowserPreferences(TypedDict, total=False):
16+
download: DownloadPreferences
17+
profile: ProfilePreferences
18+
intl: NotRequired[dict[str, str]]
19+
plugins: NotRequired[dict[str, bool]]
20+
credentials_enable_service: bool
21+
22+
23+
# Runtime schema used for validating preference paths and value types.
24+
# Keys map to either a python type (str/bool/int/dict) or to a nested dict
25+
# describing child keys and their expected types.
26+
PREFERENCE_SCHEMA: dict = {
27+
'download': {
28+
'default_directory': str,
29+
'prompt_for_download': bool,
30+
'directory_upgrade': bool,
31+
},
32+
'profile': {
33+
'password_manager_enabled': bool,
34+
# default_content_setting_values is a mapping of content name -> int
35+
'default_content_setting_values': {
36+
'popups': int,
37+
'notifications': int,
38+
'automatic_downloads': int,
39+
},
40+
},
41+
'intl': {
42+
'accept_languages': str,
43+
},
44+
'plugins': {
45+
'always_open_pdf_externally': bool,
46+
},
47+
'credentials_enable_service': bool,
48+
}

pydoll/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,18 @@ class WrongPrefsDict(PydollException):
319319
message = 'The dict can not contain "prefs" key, provide only the prefs options'
320320

321321

322+
class InvalidPreferencePath(PydollException):
323+
"""Raised when a provided preference path is invalid (segment doesn't exist)."""
324+
325+
message = 'Invalid preference path'
326+
327+
328+
class InvalidPreferenceValue(PydollException):
329+
"""Invalid value for a preference (incompatible type)"""
330+
331+
message = 'Invalid preference value'
332+
333+
322334
class ElementPreconditionError(ElementException):
323335
"""Raised when invalid or missing preconditions are provided for element operations."""
324336

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ pythonpath = "."
5858
addopts = '-p no:warnings'
5959

6060
[tool.taskipy.tasks]
61-
lint = 'ruff check .; ruff check . --diff'
62-
format = 'ruff check . --fix; ruff format .'
61+
lint = 'ruff check . && ruff check . --diff'
62+
format = 'ruff check . --fix && ruff format .'
6363
test = 'pytest -s -x --cov=pydoll -vv'
6464
post_test = 'coverage html'
6565

0 commit comments

Comments
 (0)