From 05fa1a052a8d4549d43b0f968f029b5e68290ce0 Mon Sep 17 00:00:00 2001 From: Cloak-HQ Date: Thu, 5 Mar 2026 02:13:46 +0100 Subject: [PATCH] refactor: unify timezone parameter naming across Python and JS wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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() 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 --- CHANGELOG.md | 4 ++++ README.md | 6 ++--- cloakbrowser/_version.py | 2 +- cloakbrowser/browser.py | 52 +++++++++++++++++++++++++++------------- js/README.md | 2 +- js/package.json | 2 +- js/src/playwright.ts | 13 ++++++++++ js/src/types.ts | 2 +- js/tests/config.test.ts | 29 +++++++++++++++++++++- tests/test_build_args.py | 52 ++++++++++++++++++++++++++++++++++++++-- 10 files changed, 137 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7839579..3c7ac83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index f60e0f0..4ca54b3 100644 --- a/README.md +++ b/README.md @@ -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(); diff --git a/cloakbrowser/_version.py b/cloakbrowser/_version.py index d7b30e1..8879c6c 100644 --- a/cloakbrowser/_version.py +++ b/cloakbrowser/_version.py @@ -1 +1 @@ -__version__ = "0.3.6" +__version__ = "0.3.7" diff --git a/cloakbrowser/browser.py b/cloakbrowser/browser.py index 80653b3..a081a27 100644 --- a/cloakbrowser/browser.py +++ b/cloakbrowser/browser.py @@ -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) diff --git a/js/README.md b/js/README.md index 63d70ba..feccada 100644 --- a/js/README.md +++ b/js/README.md @@ -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 diff --git a/js/package.json b/js/package.json index 17e164e..846904b 100644 --- a/js/package.json +++ b/js/package.json @@ -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", diff --git a/js/src/playwright.ts b/js/src/playwright.ts index 3c19b99..3eecd8f 100644 --- a/js/src/playwright.ts +++ b/js/src/playwright.ts @@ -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(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 { export async function launchContext( options: LaunchContextOptions = {} ): Promise { + 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 { + options = migrateTimezoneId(options); const { chromium } = await import("playwright-core"); const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary()); diff --git a/js/src/types.ts b/js/src/types.ts index 871675b..5c96b4f 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -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"; diff --git a/js/tests/config.test.ts b/js/tests/config.test.ts index d910b13..8f928f4 100644 --- a/js/tests/config.test.ts +++ b/js/tests/config.test.ts @@ -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); + }); +}); diff --git a/tests/test_build_args.py b/tests/test_build_args.py index bcb0433..201c671 100644 --- a/tests/test_build_args.py +++ b/tests/test_build_args.py @@ -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