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
- **[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",
viewport={"width": 1920, "height": 1080},
locale="en-US",
timezone_id="America/New_York",
timezone="America/New_York",
)
page = context.new_page()
page.goto("https://protected-site.com")
@@ -281,7 +281,7 @@ ctx.close() # profile saved
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()`.
@@ -327,7 +327,7 @@ const context = await launchContext({
userAgent: 'Custom UA',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
timezone: 'America/New_York',
});
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 os
import warnings
from typing import Any, Literal, TypedDict
from urllib.parse import unquote, urlparse, urlunparse
@@ -25,6 +26,17 @@ from .download import ensure_binary
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):
server: str
@@ -184,7 +196,7 @@ def launch_persistent_context(
user_agent: str | None = None,
viewport: dict | None = None,
locale: str | None = None,
timezone_id: str | None = None,
timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False,
**kwargs: Any,
@@ -206,7 +218,7 @@ def launch_persistent_context(
user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
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'.
Default: None (uses Chromium default, which is 'light').
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
timezone = _migrate_timezone_id(timezone, kwargs)
binary_path = ensure_binary()
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale)
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
logger.debug(
"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
if locale:
context_kwargs["locale"] = locale
if timezone_id:
context_kwargs["timezone_id"] = timezone_id
if timezone:
context_kwargs["timezone_id"] = timezone
if color_scheme:
context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs)
@@ -280,7 +294,7 @@ async def launch_persistent_context_async(
user_agent: str | None = None,
viewport: dict | None = None,
locale: str | None = None,
timezone_id: str | None = None,
timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False,
**kwargs: Any,
@@ -301,7 +315,7 @@ async def launch_persistent_context_async(
user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
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'.
geoip: Auto-detect timezone/locale from proxy IP (default False).
**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
timezone = _migrate_timezone_id(timezone, kwargs)
binary_path = ensure_binary()
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale)
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
logger.debug(
"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
if locale:
context_kwargs["locale"] = locale
if timezone_id:
context_kwargs["timezone_id"] = timezone_id
if timezone:
context_kwargs["timezone_id"] = timezone
if color_scheme:
context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs)
@@ -377,7 +393,7 @@ def launch_context(
user_agent: str | None = None,
viewport: dict | None = None,
locale: str | None = None,
timezone_id: str | None = None,
timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False,
**kwargs: Any,
@@ -395,7 +411,7 @@ def launch_context(
user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
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'.
Default: None (uses Chromium default, which is 'light').
Note: 'no-preference' doesn't work in Patchright (falls back to 'light').
@@ -405,9 +421,11 @@ def launch_context(
Returns:
Playwright BrowserContext object.
"""
timezone = _migrate_timezone_id(timezone, kwargs)
# Resolve geoip BEFORE launch() to avoid double-resolution and ensure
# 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
# context and interferes with Playwright's timezone_id on new contexts.
# 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
if locale:
context_kwargs["locale"] = locale
if timezone_id:
context_kwargs["timezone_id"] = timezone_id
if timezone:
context_kwargs["timezone_id"] = timezone
if color_scheme:
context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs)
+1 -1
View File
@@ -97,7 +97,7 @@ const context = await launchContext({
userAgent: 'Custom UA',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
timezone: 'America/New_York',
});
// Persistent profile — stay logged in, bypass incognito detection, load extensions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"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.",
"type": "module",
"main": "dist/index.js",
+13
View File
@@ -9,6 +9,17 @@ import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js";
import { ensureBinary } from "./download.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.
*
@@ -62,6 +73,7 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
export async function launchContext(
options: LaunchContextOptions = {}
): Promise<BrowserContext> {
options = migrateTimezoneId(options);
// Resolve geoip BEFORE launch() to avoid double-resolution
const resolved = await maybeResolveGeoip(options);
// Skip --fingerprint-timezone binary flag: it only applies to the default
@@ -117,6 +129,7 @@ export async function launchContext(
export async function launchPersistentContext(
options: LaunchPersistentContextOptions
): Promise<BrowserContext> {
options = migrateTimezoneId(options);
const { chromium } = await import("playwright-core");
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 };
/** Browser locale, e.g. "en-US". */
locale?: string;
/** Timezone, e.g. "America/New_York". */
/** @deprecated Use `timezone` (inherited from LaunchOptions) instead. */
timezoneId?: string;
/** Color scheme preference — 'light', 'dark', or 'no-preference'. */
colorScheme?: "light" | "dark" | "no-preference";
+28 -1
View File
@@ -7,7 +7,7 @@ import {
getBinaryDir,
getDownloadUrl,
} from "../src/config.js";
import { _buildArgsForTest } from "../src/playwright.js";
import { _buildArgsForTest, migrateTimezoneId } from "../src/playwright.js";
describe("config", () => {
it("CHROMIUM_VERSION matches expected format", () => {
@@ -98,3 +98,30 @@ describe("buildArgs timezone/locale", () => {
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():
@@ -44,3 +46,49 @@ def test_extra_args_preserved():
assert "--disable-gpu" in args
assert "--fingerprint-timezone=Asia/Tokyo" 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