Skip to content

Commit af8f21a

Browse files
authored
Merge pull request #204 from LucasAlvws/issue-148
feat: adding prefs options customization
2 parents f0c85a1 + 3e1dae4 commit af8f21a

File tree

9 files changed

+679
-18
lines changed

9 files changed

+679
-18
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,34 @@ from pydoll.browser.options import ChromiumOptions
316316

317317
options = ChromiumOptions()
318318
options.binary_location = '/path/to/your/chrome'
319+
options.browser_preferences = {
320+
'download': {
321+
'default_directory': '/tmp/downloads',
322+
'prompt_for_download': False
323+
},
324+
'intl': {
325+
'accept_languages': 'en-US,en,pt-BR'
326+
},
327+
'profile': {
328+
'default_content_setting_values': {
329+
'notifications': 2 # Block notifications
330+
}
331+
}
332+
}
333+
334+
options.set_default_download_directory('/tmp/downloads')
335+
options.set_accept_languages('en-US,en,pt-BR')
336+
options.prompt_for_download = False
337+
319338
browser = Chrome(options=options)
320339
```
321340

341+
**Custom Preferences**
342+
- Set download directory, language, notification blocking, PDF handling, and more
343+
- Merge multiple calls; only changed keys are updated
344+
- See [docs/features.md](docs/features.md#custom-browser-preferences) for more details
345+
346+
322347
**Browser starts after a FailedToStartBrowser error?**
323348
```python
324349
from pydoll.browser import Chrome
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Deep Dive: Custom Browser Preferences in Pydoll
2+
3+
## Overview
4+
The `browser_preferences` feature (PR #204) enables direct, fine-grained control over Chromium browser settings via the `ChromiumOptions` API. This is essential for advanced automation, testing, and scraping scenarios where default browser behavior must be customized.
5+
6+
## How It Works
7+
- `ChromiumOptions.browser_preferences` is a dictionary that maps directly to Chromium's internal preferences structure.
8+
- Preferences are merged: setting new keys updates only those keys, preserving others.
9+
- Helper methods (`set_default_download_directory`, `set_accept_languages`, etc.) are provided for common scenarios.
10+
- Preferences are applied before browser launch, ensuring all settings take effect from the start of the session.
11+
- Validation ensures only dictionaries are accepted; invalid structures raise clear errors.
12+
13+
## Example
14+
```python
15+
options = ChromiumOptions()
16+
options.browser_preferences = {
17+
'download': {'default_directory': '/tmp', 'prompt_for_download': False},
18+
'intl': {'accept_languages': 'en-US,en'},
19+
'profile': {'default_content_setting_values': {'notifications': 2}}
20+
}
21+
```
22+
23+
## Advanced Usage
24+
- **Merging:** Multiple assignments merge keys, so you can incrementally build your preferences.
25+
- **Validation:** If you pass a non-dict or use the reserved 'prefs' key, an error is raised.
26+
- **Internals:** Preferences are set via a recursive setter that creates nested dictionaries as needed.
27+
- **Integration:** Used by the browser process manager to initialize the user data directory with your custom settings.
28+
29+
## Best Practices
30+
- Use helper methods for common patterns; set `browser_preferences` directly for advanced needs.
31+
- Check Chromium documentation for available preferences: https://chromium.googlesource.com/chromium/src/+/4aaa9f29d8fe5eac55b8632fa8fcb05a68d9005b/chrome/common/pref_names.cc
32+
- Avoid setting experimental or undocumented preferences unless you know their effects.
33+
34+
## References
35+
- See `pydoll/browser/options.py` for implementation details.
36+
- See tests in `tests/test_browser/test_browser_chrome.py` for usage examples.

docs/features.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,55 @@ These network analysis capabilities make Pydoll ideal for:
662662
- **Debugging**: Identify failed requests and network issues
663663
- **Security Testing**: Analyze request/response patterns
664664

665+
## Custom Browser Preferences
666+
667+
Pydoll now supports advanced browser customization through the `ChromiumOptions.browser_preferences` property. This allows you to set any Chromium browser preference for your automation session.
668+
669+
### What You Can Customize
670+
- Download directory and prompt behavior
671+
- Accepted languages
672+
- Notification blocking
673+
- PDF handling
674+
- Any other Chromium-supported preference
675+
676+
### Example: Setting Preferences
677+
```python
678+
from pydoll.browser.chromium import Chrome
679+
from pydoll.browser.options import ChromiumOptions
680+
681+
options = ChromiumOptions()
682+
options.browser_preferences = {
683+
'download': {
684+
'default_directory': '/tmp/downloads',
685+
'prompt_for_download': False
686+
},
687+
'intl': {
688+
'accept_languages': 'en-US,en,pt-BR'
689+
},
690+
'profile': {
691+
'default_content_setting_values': {
692+
'notifications': 2 # Block notifications
693+
}
694+
}
695+
}
696+
697+
# Helper methods for common preferences
698+
options.set_default_download_directory('/tmp/downloads')
699+
options.set_accept_languages('en-US,en,pt-BR')
700+
options.prompt_for_download = False
701+
702+
browser = Chrome(options=options)
703+
```
704+
705+
You can call `browser_preferences` multiple times—new keys will be merged, not replaced.
706+
707+
### Why is this useful?
708+
- Fine-grained control for scraping, testing, or automation
709+
- Avoid popups or unwanted prompts
710+
- Match user locale or automate downloads
711+
712+
---
713+
665714
## File Upload Support
666715

667716
Seamlessly handle file uploads in your automation:

pydoll/browser/chromium/base.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import asyncio
2+
import json
3+
import os
4+
import shutil
25
from abc import ABC, abstractmethod
6+
from contextlib import suppress
37
from functools import partial
48
from random import randint
9+
from tempfile import TemporaryDirectory
510
from typing import Any, Callable, Optional, TypeVar
611

712
from pydoll.browser.interfaces import BrowserOptionsManager
@@ -81,13 +86,20 @@ def __init__(
8186
self._browser_process_manager = BrowserProcessManager()
8287
self._temp_directory_manager = TempDirectoryManager()
8388
self._connection_handler = ConnectionHandler(self._connection_port)
89+
self._backup_preferences_dir = ''
8490

8591
async def __aenter__(self) -> 'Browser':
8692
"""Async context manager entry."""
8793
return self
8894

8995
async def __aexit__(self, exc_type, exc_val, exc_tb):
9096
"""Async context manager exit with cleanup."""
97+
if self._backup_preferences_dir:
98+
user_data_dir = self._get_user_data_dir()
99+
shutil.copy2(
100+
self._backup_preferences_dir,
101+
os.path.join(user_data_dir, 'Default', 'Preferences'),
102+
)
91103
if await self._is_browser_running(timeout=2):
92104
await self.stop()
93105

@@ -117,9 +129,7 @@ async def start(self, headless: bool = False) -> Tab:
117129
proxy_config = self._proxy_manager.get_proxy_credentials()
118130

119131
self._browser_process_manager.start_browser_process(
120-
binary_location,
121-
self._connection_port,
122-
self.options.arguments,
132+
binary_location, self._connection_port, self.options.arguments
123133
)
124134
await self._verify_browser_running()
125135
await self._configure_proxy(proxy_config[0], proxy_config[1])
@@ -580,10 +590,60 @@ async def _execute_command(self, command: Command[T], timeout: int = 10) -> T:
580590

581591
def _setup_user_dir(self):
582592
"""Setup temporary user data directory if not specified in options."""
583-
if '--user-data-dir' not in [arg.split('=')[0] for arg in self.options.arguments]:
584-
# For all browsers, use a temporary directory
593+
user_data_dir = self._get_user_data_dir()
594+
if user_data_dir and self.options.browser_preferences:
595+
self._set_browser_preferences_in_user_data_dir(user_data_dir)
596+
elif not user_data_dir:
585597
temp_dir = self._temp_directory_manager.create_temp_dir()
598+
# For all browsers, use a temporary directory
586599
self.options.arguments.append(f'--user-data-dir={temp_dir.name}')
600+
if self.options.browser_preferences:
601+
self._set_browser_preferences_in_temp_dir(temp_dir)
602+
603+
def _set_browser_preferences_in_temp_dir(self, temp_dir: TemporaryDirectory):
604+
os.mkdir(os.path.join(temp_dir.name, 'Default'))
605+
preferences = self.options.browser_preferences
606+
with open(
607+
os.path.join(temp_dir.name, 'Default', 'Preferences'), 'w', encoding='utf-8'
608+
) as json_file:
609+
json.dump(preferences, json_file)
610+
611+
def _set_browser_preferences_in_user_data_dir(self, user_data_dir: str):
612+
"""
613+
Set browser preferences in the user data directory.
614+
615+
This function will:
616+
1. Create a backup of the existing Preferences file if it exists
617+
2. Create Default directory if it doesn't exist
618+
3. Write the new preferences to the Preferences file
619+
620+
Args:
621+
user_data_dir: Path to the user data directory
622+
"""
623+
default_dir = os.path.join(user_data_dir, 'Default')
624+
os.makedirs(default_dir, exist_ok=True)
625+
626+
preferences_path = os.path.join(default_dir, 'Preferences')
627+
self._backup_preferences_dir = os.path.join(default_dir, 'Preferences.backup')
628+
629+
if os.path.exists(preferences_path):
630+
# Backup existing Preferences file
631+
shutil.copy2(preferences_path, self._backup_preferences_dir)
632+
633+
preferences = {}
634+
if os.path.exists(preferences_path):
635+
with suppress(json.JSONDecodeError):
636+
with open(preferences_path, 'r', encoding='utf-8') as preferences_file:
637+
preferences = json.load(preferences_file)
638+
preferences.update(self.options.browser_preferences)
639+
with open(preferences_path, 'w', encoding='utf-8') as json_file:
640+
json.dump(preferences, json_file, indent=2)
641+
642+
def _get_user_data_dir(self) -> Optional[str]:
643+
for arg in self.options.arguments:
644+
if arg.startswith('--user-data-dir='):
645+
return arg.split('=', 1)[1]
646+
return None
587647

588648
@abstractmethod
589649
def _get_default_binary_location(self) -> str:

pydoll/browser/interfaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ def start_timeout(self) -> int:
2121
def add_argument(self, argument: str):
2222
pass
2323

24+
@property
25+
@abstractmethod
26+
def browser_preferences(self) -> dict:
27+
pass
28+
2429

2530
class BrowserOptionsManager(ABC):
2631
@abstractmethod

0 commit comments

Comments
 (0)