11from contextlib import suppress
2+ from typing import Any , Optional
23
34from pydoll .browser .interfaces import Options
5+ from pydoll .browser .preference_types import PREFERENCE_SCHEMA , BrowserPreferences
46from pydoll .constants import PageLoadState
57from 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 ):
0 commit comments