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
BrowserConfigwin over the file layer; untouched group defaults never overwrite file values — set onlyscreenand the file'sname,headless,endpoint,accept_dialogs,speedkeep working strict,interactiveandgeneration_approveparticipate when passed explicitly — an explicitFalseoverrides the file value toopolling_timeoutmerges by skip-when-None: aPrettyConfigthat leaves itNone(the default) resolves from the file layer, while an explicit0.0participates in the merge as an explicit disable — indistinguishable from unset only at theNonedefault, never at the value- File values you did not touch survive:
base_urlandmodelset 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 directoryoverrides— 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
highfinding 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 mediumandlowfindings pass with aWARNINGnaming 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_promptis 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 acceptedfalse(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_mois 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_KEYfor openai,ANTHROPIC_API_KEYfor anthropic - Invalid configuration fails loudly:
ConfigurationErrornames 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.endpointmust 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