Skip to content

feat: export build_launch_options helper for composing with existing Playwright instance#255

Open
mvanhorn wants to merge 1 commit into
CloakHQ:mainfrom
mvanhorn:feat/153-python-build-launch-options
Open

feat: export build_launch_options helper for composing with existing Playwright instance#255
mvanhorn wants to merge 1 commit into
CloakHQ:mainfrom
mvanhorn:feat/153-python-build-launch-options

Conversation

@mvanhorn

Copy link
Copy Markdown

Summary

Users who run their own sync_playwright() context can now pull CloakBrowser's launch args into it with one call instead of copying the body of launch() each release.

Why this matters

In #153, @chrisemke described maintaining scrapers that share a single Playwright instance across multiple stealth backends, and asked whether launch() could accept a playwright param or expose a lower-level helper. The maintainer reply asked the same question.

This PR takes the lower-level-helper route to keep the existing launch() signature stable. Adding a playwright= kwarg can layer on later if preferred - it composes with this helper.

Testing

  • 124 unit tests pass: test_build_args, test_config, test_proxy, test_backend, test_geoip, test_extension_loading.
  • py_compile clean across cloakbrowser/browser.py, cloakbrowser/__init__.py, examples/external_playwright.py.
  • build_launch_options() returns executable_path, headless, args, ignore_default_args; adds a proxy dict only when a proxy URL is passed.
  • launch() is refactored to delegate to build_launch_options(), so the existing call path produces identical kwargs for pw.chromium.launch().
  • The example at examples/external_playwright.py shows the sync_playwright() + build_launch_options() pattern from the maintainer reply.

Scope kept to the sync path. Async launch_async, launch_context, launch_persistent_context are untouched and can follow as a separate PR if useful.

Fixes #153

@Cloak-HQ

Copy link
Copy Markdown
Contributor

Thanks for the PR, @mvanhorn! This addresses #153 nicely.

Two things before we can merge:

  1. Missing humanize_browser() helper — The JS equivalent (feat(js): export composable launch helpers #244) exports both buildLaunchOptions and humanizeBrowser. For parity, this should also export a humanize_browser(browser, ...) so users can apply humanize to their own Playwright instance.

  2. launch_async() still has duplicated logiclaunch() now delegates to build_launch_options(), but launch_async() still has its own copy of the same arg-building code. It should delegate to build_launch_options() too, otherwise they'll diverge over time.

Let us know if you'd like to update, or we can handle it on our end.

@mvanhorn

Copy link
Copy Markdown
Author

Thanks for the careful review. Both items addressed in 17c85c3:

  1. Exported humanize_browser() and humanize_browser_async(). Callers who launched their own Playwright instance (e.g. via build_launch_options()) can now apply the stealth humanize patches with humanize_browser(browser, preset='default', config=None). The sync and async variants stay split because patch_browser and patch_browser_async are themselves separate. launch(humanize=True) and launch_async(humanize=True) now delegate to these helpers, so the patch logic stays single-sourced.

  2. launch_async() delegates to build_launch_options(). The duplicated geoip/proxy/webrtc/build_args block is gone from launch_async. It now mirrors launch() exactly: build the opts dict, log args count, await pw.chromium.launch(**opts). Dropped the # noqa: C901 since complexity is back under threshold.

  3. Tests. Added tests/test_build_launch_options.py covering: build_launch_options schema (executable_path, ignore_default_args, timezone/locale flags merged into args), both humanize helpers (with patch_browser / patch_browser_async mocked so the test doesn't need a real Browser), and launch_async delegation (asserts build_launch_options is called with forwarded kwargs and pw.chromium.launch is awaited with the returned dict). Focused suite passes; full pytest requires playwright installed which I didn't add as a dev dep here.

Let me know if you'd prefer a single humanize_browser() that auto-detects sync vs async by inspecting the Browser object instead of the two-name form; happy to switch.

…Playwright

Adds build_launch_options() (re-exported from the package) that performs every step
launch() does up to the pw.chromium.launch() call and returns the kwargs dict, so
callers can drive their own Playwright instance. launch() and launch_async() both
delegate to it so their arg-building can't diverge. Also adds humanize_browser() /
humanize_browser_async() for parity with the JS side, applying the same humanize
patching to a user-supplied browser. Closes CloakHQ#153.
@mvanhorn mvanhorn force-pushed the feat/153-python-build-launch-options branch from 17c85c3 to cab003b Compare July 8, 2026 19:33
@mvanhorn

mvanhorn commented Jul 8, 2026

Copy link
Copy Markdown
Author

Both done, and rebased onto main (force-pushed).

  1. Added humanize_browser() (and humanize_browser_async() for the async path) for parity with the JS side - they apply the same humanize patching launch(humanize=True) does to a user-supplied browser. Both re-exported from the package.
  2. launch() and launch_async() now both delegate to build_launch_options(), so their arg-building can't drift. I also folded the current launch() behavior (license_key/browser_version, start_maximized, the Windows-font warning, and env handling) into build_launch_options() so delegating doesn't drop any of it.

Verified: the build_launch_options + delegation tests pass, including a new one asserting launch_async delegates. The only failing tests locally are the real-browser bot-detection ones that need the licensed binary, and those fail the same way on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Function to launch browser with a pre-existent playwright context

2 participants