refactor: unify timezone parameter naming across Python and JS wrappers

- Rename timezone_id → timezone in launch_context(), launch_persistent_context(),
  and launch_persistent_context_async() (Python)
- Extract _migrate_timezone_id() helper for deprecation compat (DRY)
- Always pop timezone_id from kwargs to prevent override via context_kwargs.update()
- Use FutureWarning (visible by default) instead of DeprecationWarning
- JS: deprecate timezoneId on LaunchContextOptions with runtime shim
- Extract migrateTimezoneId<T>() shared helper in playwright.ts (DRY)
- Bump version to 0.3.7 in _version.py and package.json
- Add 4 Python + 4 JS unit tests for deprecation compat behavior
This commit is contained in:
Cloak-HQ
2026-03-05 02:50:55 +01:00
parent 25acff23b7
commit 05fa1a052a
10 changed files with 137 additions and 27 deletions
+4
View File
@@ -6,6 +6,10 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
--- ---
## [0.3.7] — 2026-03-05
- **[wrapper]** Unify timezone parameter: rename `timezone_id` to `timezone` in `launch_context()`, `launch_persistent_context()`, and `launch_persistent_context_async()` (Python). Old `timezone_id` still works with a deprecation warning. JS: deprecate `timezoneId` on `LaunchContextOptions` — use `timezone` (inherited from `LaunchOptions`)
## [0.3.6] — 2026-03-04 ## [0.3.6] — 2026-03-04
- **[wrapper]** `proxy` parameter now accepts a Playwright proxy dict (`{server, bypass, username, password}`) in addition to URL strings — enables bypass lists and separate auth fields (PR #24). **TS note:** type changed from `string` to `string | object` — code that assumed `proxy` is always a string may need a `typeof` narrowing check - **[wrapper]** `proxy` parameter now accepts a Playwright proxy dict (`{server, bypass, username, password}`) in addition to URL strings — enables bypass lists and separate auth fields (PR #24). **TS note:** type changed from `string` to `string | object` — code that assumed `proxy` is always a string may need a `typeof` narrowing check
+3 -3
View File
@@ -251,7 +251,7 @@ context = launch_context(
user_agent="Custom UA", user_agent="Custom UA",
viewport={"width": 1920, "height": 1080}, viewport={"width": 1920, "height": 1080},
locale="en-US", locale="en-US",
timezone_id="America/New_York", timezone="America/New_York",
) )
page = context.new_page() page = context.new_page()
page.goto("https://protected-site.com") page.goto("https://protected-site.com")
@@ -281,7 +281,7 @@ ctx.close() # profile saved
ctx = launch_persistent_context("./my-profile", headless=False) ctx = launch_persistent_context("./my-profile", headless=False)
``` ```
Supports all the same options as `launch_context()`: `proxy`, `user_agent`, `viewport`, `locale`, `timezone_id`, `color_scheme`, `geoip`. Supports all the same options as `launch_context()`: `proxy`, `user_agent`, `viewport`, `locale`, `timezone`, `color_scheme`, `geoip`.
Async version: `launch_persistent_context_async()`. Async version: `launch_persistent_context_async()`.
@@ -327,7 +327,7 @@ const context = await launchContext({
userAgent: 'Custom UA', userAgent: 'Custom UA',
viewport: { width: 1920, height: 1080 }, viewport: { width: 1920, height: 1080 },
locale: 'en-US', locale: 'en-US',
timezoneId: 'America/New_York', timezone: 'America/New_York',
}); });
const page = await context.newPage(); const page = await context.newPage();
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.3.6" __version__ = "0.3.7"
+35 -17
View File
@@ -16,6 +16,7 @@ from __future__ import annotations
import logging import logging
import os import os
import warnings
from typing import Any, Literal, TypedDict from typing import Any, Literal, TypedDict
from urllib.parse import unquote, urlparse, urlunparse from urllib.parse import unquote, urlparse, urlunparse
@@ -25,6 +26,17 @@ from .download import ensure_binary
logger = logging.getLogger("cloakbrowser") logger = logging.getLogger("cloakbrowser")
def _migrate_timezone_id(timezone: str | None, kwargs: dict[str, Any]) -> str | None:
"""Pop deprecated timezone_id from kwargs, warn, return resolved timezone."""
if "timezone_id" in kwargs:
warnings.warn("timezone_id is deprecated, use timezone instead", FutureWarning, stacklevel=3)
if timezone is None:
timezone = kwargs.pop("timezone_id")
else:
kwargs.pop("timezone_id")
return timezone
class _ProxySettingsRequired(TypedDict): class _ProxySettingsRequired(TypedDict):
server: str server: str
@@ -184,7 +196,7 @@ def launch_persistent_context(
user_agent: str | None = None, user_agent: str | None = None,
viewport: dict | None = None, viewport: dict | None = None,
locale: str | None = None, locale: str | None = None,
timezone_id: str | None = None, timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None, color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False, geoip: bool = False,
**kwargs: Any, **kwargs: Any,
@@ -206,7 +218,7 @@ def launch_persistent_context(
user_agent: Custom user agent string. user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}. viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
locale: Browser locale, e.g. "en-US". locale: Browser locale, e.g. "en-US".
timezone_id: Timezone, e.g. "America/New_York". timezone: IANA timezone (e.g. 'America/New_York').
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'. color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
Default: None (uses Chromium default, which is 'light'). Default: None (uses Chromium default, which is 'light').
geoip: Auto-detect timezone/locale from proxy IP (default False). geoip: Auto-detect timezone/locale from proxy IP (default False).
@@ -226,9 +238,11 @@ def launch_persistent_context(
""" """
from patchright.sync_api import sync_playwright from patchright.sync_api import sync_playwright
timezone = _migrate_timezone_id(timezone, kwargs)
binary_path = ensure_binary() binary_path = ensure_binary()
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale) timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale) chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
logger.debug( logger.debug(
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)", "Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
@@ -242,8 +256,8 @@ def launch_persistent_context(
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
if locale: if locale:
context_kwargs["locale"] = locale context_kwargs["locale"] = locale
if timezone_id: if timezone:
context_kwargs["timezone_id"] = timezone_id context_kwargs["timezone_id"] = timezone
if color_scheme: if color_scheme:
context_kwargs["color_scheme"] = color_scheme context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs) context_kwargs.update(kwargs)
@@ -280,7 +294,7 @@ async def launch_persistent_context_async(
user_agent: str | None = None, user_agent: str | None = None,
viewport: dict | None = None, viewport: dict | None = None,
locale: str | None = None, locale: str | None = None,
timezone_id: str | None = None, timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None, color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False, geoip: bool = False,
**kwargs: Any, **kwargs: Any,
@@ -301,7 +315,7 @@ async def launch_persistent_context_async(
user_agent: Custom user agent string. user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}. viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
locale: Browser locale, e.g. "en-US". locale: Browser locale, e.g. "en-US".
timezone_id: Timezone, e.g. "America/New_York". timezone: IANA timezone (e.g. 'America/New_York').
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'. color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
geoip: Auto-detect timezone/locale from proxy IP (default False). geoip: Auto-detect timezone/locale from proxy IP (default False).
**kwargs: Passed directly to playwright.chromium.launch_persistent_context(). **kwargs: Passed directly to playwright.chromium.launch_persistent_context().
@@ -324,9 +338,11 @@ async def launch_persistent_context_async(
""" """
from patchright.async_api import async_playwright from patchright.async_api import async_playwright
timezone = _migrate_timezone_id(timezone, kwargs)
binary_path = ensure_binary() binary_path = ensure_binary()
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale) timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale) chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
logger.debug( logger.debug(
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)", "Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
@@ -340,8 +356,8 @@ async def launch_persistent_context_async(
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
if locale: if locale:
context_kwargs["locale"] = locale context_kwargs["locale"] = locale
if timezone_id: if timezone:
context_kwargs["timezone_id"] = timezone_id context_kwargs["timezone_id"] = timezone
if color_scheme: if color_scheme:
context_kwargs["color_scheme"] = color_scheme context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs) context_kwargs.update(kwargs)
@@ -377,7 +393,7 @@ def launch_context(
user_agent: str | None = None, user_agent: str | None = None,
viewport: dict | None = None, viewport: dict | None = None,
locale: str | None = None, locale: str | None = None,
timezone_id: str | None = None, timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None, color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False, geoip: bool = False,
**kwargs: Any, **kwargs: Any,
@@ -395,7 +411,7 @@ def launch_context(
user_agent: Custom user agent string. user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}. viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
locale: Browser locale, e.g. "en-US". locale: Browser locale, e.g. "en-US".
timezone_id: Timezone, e.g. "America/New_York". timezone: IANA timezone (e.g. 'America/New_York').
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'. color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
Default: None (uses Chromium default, which is 'light'). Default: None (uses Chromium default, which is 'light').
Note: 'no-preference' doesn't work in Patchright (falls back to 'light'). Note: 'no-preference' doesn't work in Patchright (falls back to 'light').
@@ -405,9 +421,11 @@ def launch_context(
Returns: Returns:
Playwright BrowserContext object. Playwright BrowserContext object.
""" """
timezone = _migrate_timezone_id(timezone, kwargs)
# Resolve geoip BEFORE launch() to avoid double-resolution and ensure # Resolve geoip BEFORE launch() to avoid double-resolution and ensure
# resolved values flow to both binary flags AND context params # resolved values flow to both binary flags AND context params
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale) timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
# Skip --fingerprint-timezone binary flag: it only applies to the default # Skip --fingerprint-timezone binary flag: it only applies to the default
# context and interferes with Playwright's timezone_id on new contexts. # context and interferes with Playwright's timezone_id on new contexts.
# Timezone is set via browser.new_context(timezone_id=...) below instead. # Timezone is set via browser.new_context(timezone_id=...) below instead.
@@ -420,8 +438,8 @@ def launch_context(
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
if locale: if locale:
context_kwargs["locale"] = locale context_kwargs["locale"] = locale
if timezone_id: if timezone:
context_kwargs["timezone_id"] = timezone_id context_kwargs["timezone_id"] = timezone
if color_scheme: if color_scheme:
context_kwargs["color_scheme"] = color_scheme context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs) context_kwargs.update(kwargs)
+1 -1
View File
@@ -97,7 +97,7 @@ const context = await launchContext({
userAgent: 'Custom UA', userAgent: 'Custom UA',
viewport: { width: 1920, height: 1080 }, viewport: { width: 1920, height: 1080 },
locale: 'en-US', locale: 'en-US',
timezoneId: 'America/New_York', timezone: 'America/New_York',
}); });
// Persistent profile — stay logged in, bypass incognito detection, load extensions // Persistent profile — stay logged in, bypass incognito detection, load extensions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloakbrowser", "name": "cloakbrowser",
"version": "0.3.6", "version": "0.3.7",
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.", "description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
+13
View File
@@ -9,6 +9,17 @@ import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js";
import { ensureBinary } from "./download.js"; import { ensureBinary } from "./download.js";
import { parseProxyUrl } from "./proxy.js"; import { parseProxyUrl } from "./proxy.js";
/** @internal Migrate deprecated timezoneId → timezone, warn once. Exported for testing. */
export function migrateTimezoneId<T extends { timezone?: string; timezoneId?: string }>(options: T): T {
if (options.timezoneId != null) {
console.warn("[cloakbrowser] timezoneId is deprecated, use timezone instead");
const merged = { ...options, timezone: options.timezone ?? options.timezoneId };
delete (merged as any).timezoneId;
return merged;
}
return options;
}
/** /**
* Launch stealth Chromium browser via Playwright. * Launch stealth Chromium browser via Playwright.
* *
@@ -62,6 +73,7 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
export async function launchContext( export async function launchContext(
options: LaunchContextOptions = {} options: LaunchContextOptions = {}
): Promise<BrowserContext> { ): Promise<BrowserContext> {
options = migrateTimezoneId(options);
// Resolve geoip BEFORE launch() to avoid double-resolution // Resolve geoip BEFORE launch() to avoid double-resolution
const resolved = await maybeResolveGeoip(options); const resolved = await maybeResolveGeoip(options);
// Skip --fingerprint-timezone binary flag: it only applies to the default // Skip --fingerprint-timezone binary flag: it only applies to the default
@@ -117,6 +129,7 @@ export async function launchContext(
export async function launchPersistentContext( export async function launchPersistentContext(
options: LaunchPersistentContextOptions options: LaunchPersistentContextOptions
): Promise<BrowserContext> { ): Promise<BrowserContext> {
options = migrateTimezoneId(options);
const { chromium } = await import("playwright-core"); const { chromium } = await import("playwright-core");
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary()); const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
+1 -1
View File
@@ -33,7 +33,7 @@ export interface LaunchContextOptions extends LaunchOptions {
viewport?: { width: number; height: number }; viewport?: { width: number; height: number };
/** Browser locale, e.g. "en-US". */ /** Browser locale, e.g. "en-US". */
locale?: string; locale?: string;
/** Timezone, e.g. "America/New_York". */ /** @deprecated Use `timezone` (inherited from LaunchOptions) instead. */
timezoneId?: string; timezoneId?: string;
/** Color scheme preference — 'light', 'dark', or 'no-preference'. */ /** Color scheme preference — 'light', 'dark', or 'no-preference'. */
colorScheme?: "light" | "dark" | "no-preference"; colorScheme?: "light" | "dark" | "no-preference";
+28 -1
View File
@@ -7,7 +7,7 @@ import {
getBinaryDir, getBinaryDir,
getDownloadUrl, getDownloadUrl,
} from "../src/config.js"; } from "../src/config.js";
import { _buildArgsForTest } from "../src/playwright.js"; import { _buildArgsForTest, migrateTimezoneId } from "../src/playwright.js";
describe("config", () => { describe("config", () => {
it("CHROMIUM_VERSION matches expected format", () => { it("CHROMIUM_VERSION matches expected format", () => {
@@ -98,3 +98,30 @@ describe("buildArgs timezone/locale", () => {
expect(args.some(a => a.startsWith("--lang="))).toBe(false); expect(args.some(a => a.startsWith("--lang="))).toBe(false);
}); });
}); });
describe("migrateTimezoneId deprecation", () => {
it("migrates timezoneId to timezone", () => {
const result = migrateTimezoneId({ timezoneId: "Europe/Paris" });
expect(result.timezone).toBe("Europe/Paris");
expect(result).not.toHaveProperty("timezoneId");
});
it("preserves explicit timezone over timezoneId", () => {
const result = migrateTimezoneId({ timezone: "UTC", timezoneId: "Europe/Paris" });
expect(result.timezone).toBe("UTC");
expect(result).not.toHaveProperty("timezoneId");
});
it("returns options unchanged when no timezoneId", () => {
const opts = { timezone: "UTC" };
const result = migrateTimezoneId(opts);
expect(result).toBe(opts); // same reference, no copy
expect(result.timezone).toBe("UTC");
});
it("returns options unchanged when neither is set", () => {
const opts = {};
const result = migrateTimezoneId(opts);
expect(result).toBe(opts);
});
});
+50 -2
View File
@@ -1,6 +1,8 @@
"""Unit tests for _build_args timezone/locale injection.""" """Unit tests for _build_args timezone/locale injection and deprecation compat."""
from cloakbrowser.browser import _build_args import warnings
from cloakbrowser.browser import _build_args, _migrate_timezone_id
def test_timezone_injected(): def test_timezone_injected():
@@ -44,3 +46,49 @@ def test_extra_args_preserved():
assert "--disable-gpu" in args assert "--disable-gpu" in args
assert "--fingerprint-timezone=Asia/Tokyo" in args assert "--fingerprint-timezone=Asia/Tokyo" in args
assert "--lang=ja-JP" in args assert "--lang=ja-JP" in args
# --- _migrate_timezone_id deprecation compat ---
def test_migrate_old_param_only():
"""timezone_id in kwargs should be promoted to timezone."""
kwargs = {"timezone_id": "Europe/Paris"}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = _migrate_timezone_id(None, kwargs)
assert result == "Europe/Paris"
assert "timezone_id" not in kwargs
assert len(w) == 1 and issubclass(w[0].category, FutureWarning)
def test_migrate_new_param_wins():
"""Explicit timezone takes precedence; timezone_id is still popped."""
kwargs = {"timezone_id": "Europe/Paris"}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = _migrate_timezone_id("UTC", kwargs)
assert result == "UTC"
assert "timezone_id" not in kwargs
assert len(w) == 1
def test_migrate_no_old_param():
"""No warning when timezone_id is absent."""
kwargs = {"other": "value"}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = _migrate_timezone_id("UTC", kwargs)
assert result == "UTC"
assert "other" in kwargs
assert len(w) == 0
def test_migrate_both_none():
"""Neither param set — returns None, no warning."""
kwargs = {}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = _migrate_timezone_id(None, kwargs)
assert result is None
assert len(w) == 0