Skip to content

Configuration

PrettyPlay settings. For integrators configuring a test project and CI.

The immutable part of the settings lives in the [tool.prettyplay] section of pyproject.toml. Load it once per test; a test can additionally override specific values programmatically through PrettyConfig.

[tool.prettyplay]
provider = "openai"              # openai | anthropic
model = "gpt-5"
generation_model = ""            # optional: empty -> model
classification_model = ""        # optional: empty -> model
base_url = ""
cache_root = ""                  # empty -> <cwd>/.prettyplay/cache/
generation_prompt = ""           # user instructions for generation; empty -> no instructions block
generation_approve = true        # the two-dimension compliance gate (instructions + step adequacy) before caching; false -> the gate never runs (the old behavior)
classification_prompt = ""       # user instructions for classification; empty -> no instructions block
strict = false                   # true -> replay-only mode (no generation, no healing)
interactive = false              # true -> the steering dialog on a terminally stuck step (local sessions)
generation_attempts = 3
healing_attempts = 2
polling_timeout = 6.0            # settle window seconds; omit -> off, 0.0 -> explicit disable
polling_delay = 0.5              # pause between settle re-executions
send_screenshots = false

[tool.prettyplay.browser]
name = "chromium"                # chromium | firefox | webkit | chrome | msedge
screen = ""                      # "" | WxH | fullscreen | Playwright device name
headless = true                  # false -> run with a visible browser window
endpoint = ""                    # ws:// endpoint of a remote browser; empty -> local launch
accept_dialogs = false           # true -> accept (else dismiss) dialogs no in-step capture claims
speed = 100                      # pace of the run: 0-100 %, 100 — full speed (default)

The browser group settings — engines, screen modes, remote endpoints — are covered in detail in Browser setup.

Environment overrides

Every setting has an override for CI — env variable PRETTYPLAY_<SETTING> in upper case; the browser group keeps flat env names:

Setting Env override
provider PRETTYPLAY_PROVIDER
browser.name PRETTYPLAY_BROWSER_NAME
browser.screen PRETTYPLAY_BROWSER_SCREEN
browser.headless PRETTYPLAY_BROWSER_HEADLESS
browser.endpoint PRETTYPLAY_BROWSER_ENDPOINT
browser.accept_dialogs PRETTYPLAY_BROWSER_ACCEPT_DIALOGS
browser.speed PRETTYPLAY_BROWSER_SPEED
model PRETTYPLAY_MODEL
generation_model PRETTYPLAY_GENERATION_MODEL
classification_model PRETTYPLAY_CLASSIFICATION_MODEL
base_url PRETTYPLAY_BASE_URL
cache_root PRETTYPLAY_CACHE_ROOT
generation_attempts PRETTYPLAY_GENERATION_ATTEMPTS
healing_attempts PRETTYPLAY_HEALING_ATTEMPTS
polling_timeout PRETTYPLAY_POLLING_TIMEOUT
polling_delay PRETTYPLAY_POLLING_DELAY
interactive PRETTYPLAY_INTERACTIVE
send_screenshots PRETTYPLAY_SEND_SCREENSHOTS
strict PRETTYPLAY_STRICT
generation_prompt PRETTYPLAY_GENERATION_PROMPT
generation_approve PRETTYPLAY_GENERATION_APPROVE
classification_prompt PRETTYPLAY_CLASSIFICATION_PROMPT

Env values parse by the field type: booleans accept true/false/1/0 case-insensitively, integers parse as decimals, floats (the polling settings) parse as decimal floats, and an unparseable value fails loudly with a ConfigurationError naming the setting, the received value and the accepted form.

Old flat keys are gone — hard break

browser, headless and browser_endpoint at the [tool.prettyplay] level no longer exist (pre-1.0 break). A config carrying them fails loudly at load: the error names each old key and its new home — browser[tool.prettyplay.browser] name, headless[tool.prettyplay.browser] headless, browser_endpoint[tool.prettyplay.browser] endpoint. The removed legacy env name PRETTYPLAY_BROWSER fails the same way with a hint to use PRETTYPLAY_BROWSER_NAME. Migrate before upgrading.

Per-test overrides — layered merge

PrettyConfig is the public name of the full settings model. A config passed to the test object carries only the explicitly set values; everything else resolves from pyproject+env:

from prettyplay import BrowserConfig, PrettyConfig, PrettyPlay

test = PrettyPlay(
    cache_key="login-flow",
    config=PrettyConfig(
        strict=True,
        browser=BrowserConfig(screen="fullscreen", headless=False),
    ),
)
  • An explicitly set field wins over pyproject+env; a field left at its default falls back to the file layer
  • The merge reaches inside the nested group: explicitly set fields of a passed BrowserConfig win over the file layer; untouched group defaults never overwrite file values — set only screen and the file's name, headless, endpoint, accept_dialogs, speed keep working
  • strict, interactive and generation_approve participate when passed explicitly — an explicit False overrides the file value too
  • polling_timeout merges by skip-when-None: a PrettyConfig that leaves it None (the default) resolves from the file layer, while an explicit 0.0 participates in the merge as an explicit disable — indistinguishable from unset only at the None default, never at the value
  • File values you did not touch survive: base_url and model set only in pyproject.toml keep working when a config is passed
  • One model — one place of validation: file and programmatic values validate identically

The PrettyConfig API

PrettyConfig is the public name of the validated settings model (Config internally, prettyplay.config.Config); re-exported from the package root together with the nested browser group:

from prettyplay import BrowserConfig, PrettyConfig

Both models are pydantic v2 with keyword-only construction; every field carries an empty or neutral default — an empty string means unset for string fields, which is what makes the layered merge above possible.

PrettyConfig

Field Type Default Meaning
provider str "openai" LLM provider: openai or anthropic
browser BrowserConfig neutral group the nested browser settings group
model str "" the main LLM model name
generation_model str "" generation-only override; empty → model
classification_model str "" classification-only override; empty → model
base_url str "" custom LLM API endpoint
cache_root str "" empty → <cwd>/.prettyplay/cache/ resolved at load
generation_prompt str "" user instructions for generation requests; empty → no block
generation_approve bool True run the two-dimension compliance gate (instructions + step adequacy) before caching a generated step; False — the gate never runs
classification_prompt str "" user instructions for classification requests; empty → no block
strict bool False replay-only mode: no generation, no healing
interactive bool False arm the steering dialog for terminally stuck steps (local sessions)
generation_attempts int 3 generation attempt budget per step per test
healing_attempts int 2 healing attempt budget per step per test
polling_timeout float | None None settle window seconds per step execution; None/0 — polling off
polling_delay float 0.5 pause between settle re-executions
send_screenshots bool False attach screenshots to LLM requests

Read-only effective-model properties resolve the per-operation fallback:

config.effective_generation_model  # generation_model when non-empty, otherwise model
config.effective_classification_model  # classification_model when non-empty, otherwise model

BrowserConfig

Field Type Default Meaning
name str "chromium" chromium, firefox, webkit, chrome, msedge
screen str "" "" — Playwright default; WxH — fixed viewport; fullscreen; Playwright device name
headless bool True windowless local launch; ignored on a remote connect
endpoint str "" ws endpoint of a remote browser; empty — local launch
accept_dialogs bool False accept (else dismiss) dialogs no in-step stock dialog capture claims, at the run-unit tail
speed int 100 pace of the run, 0–100 inclusive; 100 — full speed (the default behavior), lower values slow the run linearly, 0 — the slowest supported pace

Fields inside the group carry no browser_ prefix — the group name scopes them; env overrides stay flat (PRETTYPLAY_BROWSER_NAME, ...). Validation: name against the five-value set, a non-empty endpoint as a ws/wss URL, positive integers for the attempts, screen at format level only (a WxH-shaped value must carry positive integers; everything else — including fullscreen and device names — passes through unresolved, because the device registry belongs to the running Playwright).

load_config

from prettyplay.config import load_config

config = load_config(pyproject_path=None, overrides=None)
  • pyproject_path — optional explicit path to pyproject.toml; None — the first pyproject.toml found upwards from the current directory
  • overrides — programmatically passed values, the same full model; None — no programmatic layer, the file layer resolves everything
  • returns the fully resolved and validated Config

Resolution order: TOML file → environment overrides (PRETTYPLAY_*) → explicitly set fields of overrides (the merge reaches inside the browser group — see Per-test overrides).

Examples

from prettyplay import BrowserConfig, PrettyConfig, PrettyPlay

# no programmatic layer — everything resolves from pyproject+env
t1 = PrettyPlay("smoke")

# per-test: strict replay with a visible fullscreen window
t2 = PrettyPlay(
    "login-flow",
    config=PrettyConfig(
        strict=True,
        browser=BrowserConfig(screen="fullscreen", headless=False),
    ),
)

# override one field of the browser group — file values for the rest keep working
t3 = PrettyPlay("login-flow", config=PrettyConfig(browser=BrowserConfig(name="firefox")))

# dedicated models per operation, one main fallback
cfg = PrettyConfig(
    model="claude-sonnet-4-5",
    generation_model="claude-opus-4-7",  # generation requests only
    classification_model="claude-haiku-4-5-20251001",  # classification requests only
)

Inspecting the effective config of a project:

from prettyplay.config import load_config

config = load_config()  # locates pyproject.toml upwards from the current directory
print(config.browser.name, config.browser.screen, config.strict)
print(config.effective_generation_model, config.effective_classification_model)

ConfigurationError

An invalid configuration never surfaces as a raw pydantic error: the loader wraps it into ConfigurationError (from prettyplay.config import ConfigurationError — derives from PrettyplayError, so the single library except clause catches it; a configuration failure is not a step failure and never carries a verdict). The message is actionable — one line per invalid setting: the setting name, the received value and the allowed values; the original pydantic ValidationError stays chained for debugging.

Instructions

A non-empty generation_prompt is sent verbatim as a USER INSTRUCTIONS block with every generation and regeneration request — it steers the style of the generated code, never the failure classification.

A non-empty classification_prompt works the same way for classification requests only — it steers the verdict explanations (e.g. answer in Russian), never generation.

The generation instructions are binding, not advisory — see The compliance gate below.

Neither ever invalidates cached steps — a cached step runs unchanged.

The compliance gate

The user instructions of generation_prompt are binding for generated step code: every successfully executed candidate passes an independent compliance check before it is cached. The check judges two dimensions in one verdict request — instruction compliance (the code against the generation_prompt instructions) and step adequacy (the code must accomplish what the step sentence says for its step type — an action step whose code only checks an already-achieved state fails it, judged from the step type and the verbatim per-step attempt history of what was already tried). The gate is the default — the deliberate opt-out default keeps loud errors instead of silent ignoring; switch it off consciously when the extra LLM call per successful generation matters more than the enforcement.

[tool.prettyplay]
generation_prompt = "Prefer id attributes for locating elements"
generation_approve = true   # default; false -> the gate never runs (the old behavior)

Env override — booleans parse true/false/1/0 case-insensitively; an unparseable value fails loudly with a ConfigurationError naming the setting, the received value and the accepted form:

export PRETTYPLAY_GENERATION_APPROVE=false

Per-test override — an explicit False wins over the file layer (the strict/interactive pattern):

config = PrettyConfig(generation_approve=False)
scenario = PrettyPlay(cache_key="smoke", config=config)

While the gate is on:

  • every successful generation costs one extra LLM call — the verdict request, through the effective classification model
  • a high finding in either dimension fails the attempt — the violation text (instruction violation: … / adequacy violation: …) joins the attempt record's error and the retry carries the grown attempt history, so the model fixes it targeted
  • medium and low findings pass with a WARNING naming the instructions
  • a JSON syntax glitch of the verdict answer is salvaged once before the validation — a model dropping a quote, a comma or a bracket does not fail the run; a malformed verdict — what the salvage cannot shape into the required findings — is a loud hard failure (ComplianceVerdictError) — a candidate is never cached unchecked
  • the gate never runs on replayed cached code: the instructions take no part in the step address, so changing them requires a manual cache purge
  • the gate never runs when generation_prompt is empty, regardless of the switch

See Step cache for the gate-before-caching invariant across all caching paths.

Dialogs

accept_dialogs of the browser group controls how the resolver of last resort settles unclaimed dialogs:

  • true — every dialog that no in-step stock dialog capture claims is accepted
  • false (default) — unclaimed dialogs are dismissed (the Playwright default outcome)
  • The resolution runs at the tail of the driver-thread run unit, not at the moment the dialog fires: a dialog unclaimed by the step blocks the page until the unit ends, which can fail the remainder of the step
  • A dialog claimed by a step's in-step stock capture is accepted or dismissed by the step itself — the setting does not apply to captured dialogs

Pace

speed of the browser group (a percentage, 0–100 inclusive, default 100) controls how fast the browser executes the run: 100 — full speed, exactly the default behavior; lower values slow the run down linearly — 0 is the slowest supported pace. The mapping is Playwright's native slow_mo: the browser inserts int((100 − speed) × 30) ms between its operations. It is an ordinary layered setting — file, env (PRETTYPLAY_BROWSER_SPEED), per-test override:

from prettyplay import BrowserConfig, PrettyConfig, PrettyPlay

test = PrettyPlay(
    "demo",
    config=PrettyConfig(browser=BrowserConfig(speed=40)),
)
  • One value per test, fixed for the whole run — it applies at browser start in every launch mode (local headed, local headless, remote connect); replays and strict runs take it identically
  • The fixed-delays rule for generated step code is untouched: slow_mo is a browser-process start parameter, not a code wait; group blocks slow down through their own library-level pauses instead — see Groups
  • An out-of-range or malformed value fails at configuration load — the error names the dotted setting browser.speed, the received value and the accepted form (an integer 0-100 inclusive); never a silent ignore

Strict mode

strict = true (env PRETTYPLAY_STRICT, per-test override) switches the run to replay-only: cached code executes honestly and nothing is ever (re)generated. A cache miss fails as an incurable step; a failed cached step is at most classified — never regenerated. Classification is the only LLM call strict mode makes; without LLM access the failure raises immediately by step type — see Failure taxonomy and Getting started.

Rules

  • LLM API keys are never stored in the config file — secrets come only from environment variables: OPENAI_API_KEY for openai, ANTHROPIC_API_KEY for anthropic
  • Invalid configuration fails loudly: ConfigurationError names the setting, the received value and the allowed values; the raw pydantic error stays chained for debugging
  • The provider set: openai, anthropic; the browser name set: chromium, firefox, webkit, chrome, msedge
  • A non-empty browser.endpoint must be a valid ws/wss URL
  • The cache root default: <cwd>/.prettyplay/cache/ — anchored at the working directory of the run, wherever the pyproject.toml was found