prettyplay 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of prettyplay might be problematic. Click here for more details.
- prettyplay/.usages/lifecycle.md +50 -0
- prettyplay/.usages/steps.md +56 -0
- prettyplay/CODEMANIFEST +182 -0
- prettyplay/__init__.py +13 -0
- prettyplay/cache/.usages/addressing.md +31 -0
- prettyplay/cache/.usages/budgets.md +21 -0
- prettyplay/cache/.usages/storage.md +32 -0
- prettyplay/cache/CODEMANIFEST +152 -0
- prettyplay/cache/__init__.py +8 -0
- prettyplay/cache/budgets.py +73 -0
- prettyplay/cache/models.py +60 -0
- prettyplay/cache/store.py +260 -0
- prettyplay/cache/text.py +35 -0
- prettyplay/config/.usages/configuration.md +87 -0
- prettyplay/config/CODEMANIFEST +132 -0
- prettyplay/config/__init__.py +6 -0
- prettyplay/config/loader.py +192 -0
- prettyplay/config/models.py +92 -0
- prettyplay/driver/.usages/facade.md +66 -0
- prettyplay/driver/CODEMANIFEST +162 -0
- prettyplay/driver/__init__.py +6 -0
- prettyplay/driver/page.py +350 -0
- prettyplay/driver/session.py +288 -0
- prettyplay/engine/.usages/generation.md +45 -0
- prettyplay/engine/.usages/healing.md +31 -0
- prettyplay/engine/CODEMANIFEST +215 -0
- prettyplay/engine/__init__.py +8 -0
- prettyplay/engine/classification.py +69 -0
- prettyplay/engine/execution.py +25 -0
- prettyplay/engine/generator.py +318 -0
- prettyplay/engine/healer.py +116 -0
- prettyplay/engine/text.py +19 -0
- prettyplay/executor.py +109 -0
- prettyplay/failures/.usages/taxonomy.md +40 -0
- prettyplay/failures/CODEMANIFEST +117 -0
- prettyplay/failures/__init__.py +17 -0
- prettyplay/failures/errors.py +147 -0
- prettyplay/llm/.usages/classification.md +25 -0
- prettyplay/llm/.usages/providers.md +33 -0
- prettyplay/llm/CODEMANIFEST +125 -0
- prettyplay/llm/__init__.py +8 -0
- prettyplay/llm/_request.py +216 -0
- prettyplay/llm/anthropic_provider.py +213 -0
- prettyplay/llm/models.py +22 -0
- prettyplay/llm/openai_provider.py +187 -0
- prettyplay/llm/provider.py +115 -0
- prettyplay/reporting/.usages/hooks.md +41 -0
- prettyplay/reporting/CODEMANIFEST +79 -0
- prettyplay/reporting/__init__.py +6 -0
- prettyplay/reporting/hooks.py +37 -0
- prettyplay/reporting/reporter.py +70 -0
- prettyplay/runtime.py +107 -0
- prettyplay/scenario.py +291 -0
- prettyplay-0.0.0.dist-info/METADATA +236 -0
- prettyplay-0.0.0.dist-info/RECORD +58 -0
- prettyplay-0.0.0.dist-info/WHEEL +5 -0
- prettyplay-0.0.0.dist-info/licenses/LICENSE +28 -0
- prettyplay-0.0.0.dist-info/top_level.txt +1 -0
prettyplay/scenario.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""The integrator-facing object: one PrettyTest per test owns the addressing and the page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
|
|
9
|
+
from .cache import StepCache
|
|
10
|
+
from .config import PrettyConfig, load_config
|
|
11
|
+
from .driver import PageFacade
|
|
12
|
+
from .engine import StepGenerator, StepHealer
|
|
13
|
+
from .executor import StepExecutor
|
|
14
|
+
from .failures.errors import PrettyplayError
|
|
15
|
+
from .reporting import StepHooks, StepReporter
|
|
16
|
+
from .runtime import PrettyplayRuntime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _raise_folded(error: PrettyplayError) -> types.NoReturn:
|
|
20
|
+
"""Re-raise a library failure with its traceback folded to this boundary.
|
|
21
|
+
|
|
22
|
+
The head link of the caught traceback is the facade method's own frame;
|
|
23
|
+
the folded traceback keeps exactly that link, so the internal library
|
|
24
|
+
frames (engine, healing, provider) never appear in what the runner shows.
|
|
25
|
+
The same exception object is re-raised — never a copy — keeping identity
|
|
26
|
+
for hook consumers and ``except`` clauses, and adding no context nesting.
|
|
27
|
+
The chained exceptions (``__context__``/``__cause__``) keep their identity
|
|
28
|
+
and messages — the original step failure stays visible for debugging —
|
|
29
|
+
but their tracebacks are folded away too: a runner that renders the chain
|
|
30
|
+
shows no internal frames either.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
error: the library failure leaving ``action``/``assertion``.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
Always: the given error with the folded traceback attached.
|
|
37
|
+
"""
|
|
38
|
+
tb = error.__traceback__
|
|
39
|
+
_fold_chain_tracebacks(error)
|
|
40
|
+
|
|
41
|
+
folded = types.TracebackType(
|
|
42
|
+
tb_next=None,
|
|
43
|
+
tb_frame=tb.tb_frame,
|
|
44
|
+
tb_lasti=tb.tb_lasti,
|
|
45
|
+
tb_lineno=tb.tb_lineno,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
raise error.with_traceback(folded)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _fold_chain_tracebacks(error: BaseException) -> None:
|
|
52
|
+
"""Drop the tracebacks of the chained exceptions, keeping the chains themselves.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
error: the exception whose ``__context__``/``__cause__`` chains are folded.
|
|
56
|
+
"""
|
|
57
|
+
pending = [error]
|
|
58
|
+
seen: set[int] = set()
|
|
59
|
+
|
|
60
|
+
while pending:
|
|
61
|
+
current = pending.pop()
|
|
62
|
+
|
|
63
|
+
if id(current) in seen:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
seen.add(id(current))
|
|
67
|
+
|
|
68
|
+
for link in (current.__context__, current.__cause__):
|
|
69
|
+
if link is not None:
|
|
70
|
+
link.__traceback__ = None
|
|
71
|
+
pending.append(link)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PrettyTest:
|
|
75
|
+
"""The main object of the integrator: one instance per UI test.
|
|
76
|
+
|
|
77
|
+
The test writes its scenario in plain sentences — :meth:`action` and
|
|
78
|
+
:meth:`assertion` — and this object does the rest: addressing of the
|
|
79
|
+
cached steps by the context key, the isolated page of the test, and the
|
|
80
|
+
full step cycle delegated to the executor. Construction is cheap: the
|
|
81
|
+
page opens lazily on the first step and no LLM credential is needed.
|
|
82
|
+
Every test composes its own runtime here — no process-wide state, no
|
|
83
|
+
state leaks between tests; two tests in one process hold two runtimes,
|
|
84
|
+
two attempt registries and two browser sessions.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
_runtime: the composition root owned by this one test.
|
|
88
|
+
_reporter: the visibility point of this test.
|
|
89
|
+
_cache: the step cache of this test.
|
|
90
|
+
_generator: the generation engine of this test.
|
|
91
|
+
_healer: the healing engine of this test.
|
|
92
|
+
_executor: the owner of the step cycle of this test.
|
|
93
|
+
_page: the isolated page of this test; ``None`` until the first step.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, cache_key: str, cache_path: str | None = None, config: PrettyConfig | None = None) -> None:
|
|
97
|
+
"""Compose the per-test objects over the runtime this test owns.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
cache_key: the context key of the test; the addressing part of
|
|
101
|
+
every cached step of this scenario.
|
|
102
|
+
cache_path: the optional subdirectory inside the cache; part of
|
|
103
|
+
the address, so steps of different subdirectories never
|
|
104
|
+
collide.
|
|
105
|
+
config: the programmatic settings layer; explicitly set values
|
|
106
|
+
win over the pyproject+env file layer, unset and empty
|
|
107
|
+
fields resolve from it. ``None`` resolves everything from
|
|
108
|
+
the file layer, as before.
|
|
109
|
+
"""
|
|
110
|
+
effective = load_config(None, config)
|
|
111
|
+
self._runtime = PrettyplayRuntime(effective)
|
|
112
|
+
self._reporter = StepReporter(hooks=[])
|
|
113
|
+
self._cache = StepCache(self._runtime.config, cache_path, self._reporter)
|
|
114
|
+
|
|
115
|
+
self._generator = StepGenerator(
|
|
116
|
+
self._runtime.config,
|
|
117
|
+
self._runtime.provider,
|
|
118
|
+
self._cache,
|
|
119
|
+
self._runtime.budgets,
|
|
120
|
+
self._reporter,
|
|
121
|
+
)
|
|
122
|
+
self._healer = StepHealer(
|
|
123
|
+
self._runtime.config,
|
|
124
|
+
self._runtime.provider,
|
|
125
|
+
self._generator,
|
|
126
|
+
self._cache,
|
|
127
|
+
self._runtime.budgets,
|
|
128
|
+
self._reporter,
|
|
129
|
+
)
|
|
130
|
+
self._executor = StepExecutor(
|
|
131
|
+
cache_key,
|
|
132
|
+
self._cache,
|
|
133
|
+
self._generator,
|
|
134
|
+
self._healer,
|
|
135
|
+
self._runtime.budgets,
|
|
136
|
+
self._reporter,
|
|
137
|
+
)
|
|
138
|
+
self._page: PageFacade | None = None
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def cache_key(self) -> str:
|
|
142
|
+
"""The context key of this test, for diagnostics."""
|
|
143
|
+
return self._executor.cache_key
|
|
144
|
+
|
|
145
|
+
def _ensure_page(self) -> PageFacade:
|
|
146
|
+
"""Open the isolated page of this test lazily, exactly once.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The page facade of this test.
|
|
150
|
+
"""
|
|
151
|
+
if self._page is None:
|
|
152
|
+
self._page = self._runtime.open_page()
|
|
153
|
+
return self._page
|
|
154
|
+
|
|
155
|
+
def action(self, text: str) -> None:
|
|
156
|
+
"""Execute one action step sentence through the step cycle.
|
|
157
|
+
|
|
158
|
+
A library failure leaving this method carries its traceback folded to
|
|
159
|
+
this boundary: the runner sees the test frame and the boundary frame
|
|
160
|
+
with the rendered message, never the internal engine frames.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
text: the sentence of the action as written by the engineer.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ProductDefectError: the step expectation is genuinely broken in the product.
|
|
167
|
+
IncurableStepError: the step never generated successfully, or the verdict
|
|
168
|
+
says regeneration cannot help.
|
|
169
|
+
LlmUnavailableError: the provider service failed; no retry.
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
self._executor.execute(text, "action", self._ensure_page())
|
|
173
|
+
except PrettyplayError as error:
|
|
174
|
+
_raise_folded(error)
|
|
175
|
+
|
|
176
|
+
def assertion(self, text: str) -> None:
|
|
177
|
+
"""Execute one assertion step sentence through the step cycle.
|
|
178
|
+
|
|
179
|
+
A library failure leaving this method carries its traceback folded to
|
|
180
|
+
this boundary, exactly as :meth:`action` does.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
text: the sentence of the assertion as written by the engineer.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ProductDefectError: the step expectation is genuinely broken in the product.
|
|
187
|
+
IncurableStepError: the step never generated successfully, or the verdict
|
|
188
|
+
says regeneration cannot help.
|
|
189
|
+
LlmUnavailableError: the provider service failed; no retry.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
self._executor.execute(text, "assertion", self._ensure_page())
|
|
193
|
+
except PrettyplayError as error:
|
|
194
|
+
_raise_folded(error)
|
|
195
|
+
|
|
196
|
+
def get_screenshot(self) -> bytes:
|
|
197
|
+
"""Return a full-page PNG screenshot of the current test page.
|
|
198
|
+
|
|
199
|
+
The decision to take a screenshot belongs to the author: nothing is
|
|
200
|
+
captured automatically on step failures.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The PNG image bytes of the page.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
PrettyplayError: no test page exists yet — run a step first.
|
|
207
|
+
"""
|
|
208
|
+
if self._page is None:
|
|
209
|
+
raise PrettyplayError("no test page yet: run a step first — the page opens lazily on the first step")
|
|
210
|
+
|
|
211
|
+
return self._page.screenshot()
|
|
212
|
+
|
|
213
|
+
def save_screenshot(self, filepath: str) -> None:
|
|
214
|
+
"""Save a full-page PNG screenshot of the current test page to a file.
|
|
215
|
+
|
|
216
|
+
Parent directories are not created: nothing is created silently —
|
|
217
|
+
a missing directory is a loud failure.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
filepath: the destination path of the PNG file.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
PrettyplayError: no test page exists yet, or the file cannot be
|
|
224
|
+
written; the original ``OSError`` is chained.
|
|
225
|
+
"""
|
|
226
|
+
if self._page is None:
|
|
227
|
+
raise PrettyplayError("no test page yet: run a step first — the page opens lazily on the first step")
|
|
228
|
+
|
|
229
|
+
image = self._page.screenshot()
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
Path(filepath).write_bytes(image)
|
|
233
|
+
except OSError as error:
|
|
234
|
+
raise PrettyplayError(f"cannot write the screenshot to {filepath}: {error}") from error
|
|
235
|
+
|
|
236
|
+
def add_hooks(self, hooks: StepHooks) -> None:
|
|
237
|
+
"""Register integrator hooks for the events of this test.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
hooks: the hooks object; register before the first step to see
|
|
241
|
+
every event of the scenario.
|
|
242
|
+
"""
|
|
243
|
+
self._reporter.hooks.append(hooks)
|
|
244
|
+
|
|
245
|
+
def close(self) -> None:
|
|
246
|
+
"""Close the page and the whole runtime of this test.
|
|
247
|
+
|
|
248
|
+
The isolated page context closes first, then the runtime stops
|
|
249
|
+
unconditionally — a failing page close (e.g. after a browser crash)
|
|
250
|
+
never keeps the browser of the test alive; its error still propagates.
|
|
251
|
+
The browser of this test does not outlive the test.
|
|
252
|
+
|
|
253
|
+
Idempotent and safe before the first step: nothing was opened —
|
|
254
|
+
nothing is closed beyond the no-op runtime close. The page reference
|
|
255
|
+
drops before its close runs, so even a failing page close never
|
|
256
|
+
repeats on a retried ``close``.
|
|
257
|
+
"""
|
|
258
|
+
page = self._page
|
|
259
|
+
self._page = None
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
if page is not None:
|
|
263
|
+
page.close()
|
|
264
|
+
finally:
|
|
265
|
+
self._runtime.close()
|
|
266
|
+
|
|
267
|
+
def __enter__(self) -> PrettyTest:
|
|
268
|
+
"""Enter the scenario block of one test.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
This test object.
|
|
272
|
+
"""
|
|
273
|
+
return self
|
|
274
|
+
|
|
275
|
+
def __exit__(
|
|
276
|
+
self,
|
|
277
|
+
exc_type: type[BaseException] | None,
|
|
278
|
+
exc_value: BaseException | None,
|
|
279
|
+
traceback: TracebackType | None,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Leave the scenario block: close the test and never suppress.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
exc_type: the type of the block exception, if any.
|
|
285
|
+
exc_value: the block exception, if any.
|
|
286
|
+
traceback: the traceback of the block exception, if any.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Nothing — a falsy return keeps the exception propagating.
|
|
290
|
+
"""
|
|
291
|
+
self.close()
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prettyplay
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: AI-powered UI test generation tool
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pydantic>=2.7
|
|
18
|
+
Requires-Dist: playwright>=1.49
|
|
19
|
+
Requires-Dist: openai>=1.30
|
|
20
|
+
Requires-Dist: anthropic>=0.28
|
|
21
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-mock>=3.10; extra == "test"
|
|
26
|
+
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
27
|
+
Provides-Extra: docs
|
|
28
|
+
Requires-Dist: mkdocs>=1.6.0; extra == "docs"
|
|
29
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# prettyplay
|
|
33
|
+
|
|
34
|
+
UI tests written as plain sentences. Each step sentence is turned into executable
|
|
35
|
+
code once — by an LLM, against the live page — and cached in the repository.
|
|
36
|
+
Every later run replays the cached code with no LLM involvement at all.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install prettyplay
|
|
42
|
+
playwright install # browser binaries for the driver
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10+.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from prettyplay import PrettyTest
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_login():
|
|
54
|
+
t = PrettyTest("login-flow")
|
|
55
|
+
t.action("открыть страницу логина")
|
|
56
|
+
t.action("ввести логин и пароль")
|
|
57
|
+
t.action("нажать «Войти»")
|
|
58
|
+
t.assertion("появилась надпись «Добро пожаловать»")
|
|
59
|
+
t.close()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or with the context manager:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
with PrettyTest("login-flow") as t:
|
|
66
|
+
t.action("открыть страницу логина")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each `PrettyTest` is fully self-contained: it owns its settings, its attempt
|
|
70
|
+
budgets and its own browser session. `close()` (or leaving the `with` block)
|
|
71
|
+
closes the page and stops the whole browser of that test, and every test
|
|
72
|
+
starts with fresh attempt budgets. The old process-wide
|
|
73
|
+
`prettyplay.get_runtime()` singleton was removed — build
|
|
74
|
+
`prettyplay.PrettyplayRuntime(config)` directly if you composed objects over
|
|
75
|
+
it.
|
|
76
|
+
|
|
77
|
+
The constructor arguments form the cache address: `cache_key` (mandatory) and
|
|
78
|
+
`cache_path` (optional subdirectory). Equal keys in the shared root reuse one
|
|
79
|
+
cached step across tests; a different language, step type or key is a
|
|
80
|
+
different step.
|
|
81
|
+
|
|
82
|
+
The third argument overrides settings per test — only the fields you pass
|
|
83
|
+
count: explicitly set values win over pyproject.toml and the environment,
|
|
84
|
+
everything else resolves from the file layer as before:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from prettyplay import PrettyConfig
|
|
88
|
+
|
|
89
|
+
t = PrettyTest("login-flow", config=PrettyConfig(browser="firefox"))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Screenshots belong to the author — nothing is captured automatically. Both
|
|
93
|
+
methods need a step to have run (the page opens lazily):
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
png = t.get_screenshot() # full-page PNG bytes
|
|
97
|
+
t.save_screenshot("artifacts/home.png") # write full-page PNG to a file
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## What happens on a step
|
|
101
|
+
|
|
102
|
+
- **cache hit** — the cached code runs; no LLM is contacted
|
|
103
|
+
- **cache miss** — the step code is generated (a candidate that must actually
|
|
104
|
+
work on the page), then cached; only successes are cached. A candidate
|
|
105
|
+
assertion that legitimately fails stops the retries at once and is
|
|
106
|
+
classified: a real defect fails as `product_defect` instead of burning the
|
|
107
|
+
attempt budget
|
|
108
|
+
- **cached failure** — the failure is classified:
|
|
109
|
+
- `rot` (the UI changed) — the step is regenerated and the cache rewritten
|
|
110
|
+
- `product_defect` — the test fails loudly; nothing is regenerated
|
|
111
|
+
- `incurable` — the step fails with an explanation and a recommendation
|
|
112
|
+
|
|
113
|
+
## Seeing the scenario
|
|
114
|
+
|
|
115
|
+
Step sentences go to the `prettyplay` logger at info level. The library
|
|
116
|
+
configures no handlers — enable logging to see the scenario in the output:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import logging
|
|
120
|
+
|
|
121
|
+
logging.basicConfig(level=logging.INFO) # plain unittest runs
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ini
|
|
125
|
+
# pytest: --log-cli-level=INFO
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
The `[tool.prettyplay]` section of pyproject.toml:
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
[tool.prettyplay]
|
|
134
|
+
provider = "openai" # openai | anthropic
|
|
135
|
+
browser = "chromium" # chromium | firefox | webkit | chrome | msedge
|
|
136
|
+
model = "gpt-5"
|
|
137
|
+
generation_model = "" # optional: empty -> model
|
|
138
|
+
classification_model = "" # optional: empty -> model
|
|
139
|
+
base_url = ""
|
|
140
|
+
cache_root = "" # empty -> <repo>/.prettyplay/cache/
|
|
141
|
+
generation_prompt = "" # user instructions for generation; empty -> no instructions block
|
|
142
|
+
browser_endpoint = "" # ws:// endpoint of a remote browser; empty -> local launch
|
|
143
|
+
generation_attempts = 3
|
|
144
|
+
healing_attempts = 2
|
|
145
|
+
send_screenshots = false
|
|
146
|
+
headless = true # false -> run with a visible browser window
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`chrome` and `msedge` launch the locally installed browser through the
|
|
150
|
+
chromium engine; the browser must be installed on the machine.
|
|
151
|
+
|
|
152
|
+
A non-empty `generation_prompt` is sent verbatim as a `USER INSTRUCTIONS`
|
|
153
|
+
block with every generation and regeneration request — it steers the style of
|
|
154
|
+
the generated code (e.g. `prefer data-test-id attributes`), never the failure
|
|
155
|
+
classification. The instructions are not part of the cache address: changing
|
|
156
|
+
them never invalidates cached steps — a cached step runs unchanged.
|
|
157
|
+
|
|
158
|
+
A non-empty `browser_endpoint` (e.g. `ws://ci-grid:3000/playwright/chromium`)
|
|
159
|
+
connects to a remote Playwright Server or browser grid instead of launching
|
|
160
|
+
locally: `headless` does not apply to a connect (window visibility belongs to
|
|
161
|
+
the endpoint server) and `chrome`/`msedge` map to the chromium engine —
|
|
162
|
+
channels are a local-launch concept. A non-empty endpoint must be a valid
|
|
163
|
+
ws/wss URL (otherwise `ConfigurationError` names the setting), and a failed
|
|
164
|
+
connect fails loudly with the endpoint in the message.
|
|
165
|
+
|
|
166
|
+
Every setting has a `PRETTYPLAY_<SETTING_UPPER>` environment override for CI,
|
|
167
|
+
except the browser (`PRETTYPLAY_BROWSER_NAME`) and headless
|
|
168
|
+
(`PRETTYPLAY_BROWSER_HEADLESS`). The removed legacy name `PRETTYPLAY_BROWSER`
|
|
169
|
+
fails immediately with a hint to use `PRETTYPLAY_BROWSER_NAME`.
|
|
170
|
+
|
|
171
|
+
An invalid setting fails loudly with a `ConfigurationError`: one line per
|
|
172
|
+
setting — the name, the received value and the allowed values.
|
|
173
|
+
|
|
174
|
+
LLM API keys are never stored in the config file: they come only from the
|
|
175
|
+
environment — `OPENAI_API_KEY` for openai, `ANTHROPIC_API_KEY` for anthropic —
|
|
176
|
+
and are read lazily on the first request.
|
|
177
|
+
|
|
178
|
+
## Failure taxonomy
|
|
179
|
+
|
|
180
|
+
Every library failure derives from `PrettyplayError`:
|
|
181
|
+
|
|
182
|
+
| Exception | Meaning | Recommended reaction |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| ProductDefectError | real product regression | treat as a bug: this failure is the value of the suite |
|
|
185
|
+
| IncurableStepError | the step cannot be (re)generated | follow `recommendation`: reword the step or refresh the cache |
|
|
186
|
+
| LlmUnavailableError | LLM infrastructure down | restore provider access; cached steps are unaffected |
|
|
187
|
+
| ConfigurationError | invalid `[tool.prettyplay]` settings | fix the named setting — the message lists received and allowed values |
|
|
188
|
+
|
|
189
|
+
`ProductDefectError` and `IncurableStepError` carry a verdict — `category`,
|
|
190
|
+
`explanation`, `recommendation` from the failure classification — appended to
|
|
191
|
+
the exception message and delivered through the `on_step_verdict` hook.
|
|
192
|
+
`ProductDefectError` also derives from `AssertionError`, so any runner counts
|
|
193
|
+
it as a failed test, never an error. Tracebacks of library failures are folded
|
|
194
|
+
at the `t.action(...)` / `t.assertion(...)` call site: internal engine frames
|
|
195
|
+
never appear in what the runner shows.
|
|
196
|
+
|
|
197
|
+
A healed run never turns a `ProductDefectError` into a green test.
|
|
198
|
+
|
|
199
|
+
## Hooks
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from prettyplay.reporting import StepHooks
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class Reporter(StepHooks):
|
|
206
|
+
def on_step_started(self, step_text: str, step_type: str) -> None: ...
|
|
207
|
+
def on_step_passed(self, step_text: str, step_type: str) -> None: ...
|
|
208
|
+
def on_step_failed(self, step_text: str, step_type: str, error: str) -> None: ...
|
|
209
|
+
def on_step_verdict(self, step_text: str, category: str, explanation: str, recommendation: str) -> None: ...
|
|
210
|
+
def on_generation_started(self, step_text: str, attempt: int) -> None: ...
|
|
211
|
+
def on_healing_started(self, step_text: str, category: str) -> None: ...
|
|
212
|
+
def on_healed(self, step_text: str, explanation: str) -> None: ...
|
|
213
|
+
def on_cache_saved(self, step_text: str, filename: str) -> None: ...
|
|
214
|
+
def on_cache_skipped(self, step_text: str, reason: str) -> None: ...
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
t = PrettyTest("login-flow")
|
|
218
|
+
t.add_hooks(Reporter())
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A raising hook never fails the run; the failure is logged. `on_step_verdict`
|
|
222
|
+
fires after `on_step_failed`, only when the terminal failure carried a verdict.
|
|
223
|
+
|
|
224
|
+
## Cache and CI workflow
|
|
225
|
+
|
|
226
|
+
The cache lives under `.prettyplay/cache/` as plain Python files — one per
|
|
227
|
+
step, carrying its metadata (step sentence, cache key, step type, creation
|
|
228
|
+
date) and the step code.
|
|
229
|
+
|
|
230
|
+
Generate locally where the LLM is reachable → commit the cache directory →
|
|
231
|
+
CI runs the whole suite from the cache with no LLM keys at all.
|
|
232
|
+
|
|
233
|
+
## Limitations
|
|
234
|
+
|
|
235
|
+
Step sentences land in the repository cache, the logs and the LLM requests:
|
|
236
|
+
never put secrets or personal data into a step.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
prettyplay/CODEMANIFEST,sha256=J8KN00rNtkG7hA70qLSvxhBoCRiMVjH1l_kouZu5X1g,9781
|
|
2
|
+
prettyplay/__init__.py,sha256=pjAKVu2otS-4MqnTlIyXjoM54_L4TkwwMVd8bkhrRYs,393
|
|
3
|
+
prettyplay/executor.py,sha256=AJ5JApn24yop2e9mFEgC03qqZ2amAnYH-Hh4KQMakpU,4825
|
|
4
|
+
prettyplay/runtime.py,sha256=-HvXhykBa8MoLa7Tq-TWFG0I8X8u9Ql9SX6T2iC6qyQ,4203
|
|
5
|
+
prettyplay/scenario.py,sha256=dtmP4ol_QeHE5ia2LV9NG3AzfLLlfwLBbXlnLQPtlis,10691
|
|
6
|
+
prettyplay/.usages/lifecycle.md,sha256=XRHXkSef2tQU-901F6bPQFDYN981zBBic4B6uiP6-KY,3561
|
|
7
|
+
prettyplay/.usages/steps.md,sha256=okTZ8Gmf7AY2hOcw4TqL_c8Oq3mphNY8myuuaU2mWM4,1903
|
|
8
|
+
prettyplay/cache/CODEMANIFEST,sha256=fvCVBwA1hmYWbjps3x_4rh6agPMo_bmqkFCLtrzJhUk,6897
|
|
9
|
+
prettyplay/cache/__init__.py,sha256=GYhRRVLAlRCdbKuzo-bMqmaBd4UqQXwch7RUG9zbDO0,318
|
|
10
|
+
prettyplay/cache/budgets.py,sha256=Q2BDNknt309G-2oXY2IL0NxxpgFoIDDuEJC_yr7t8Ac,2819
|
|
11
|
+
prettyplay/cache/models.py,sha256=Qkj8re-CUVD68NUdTZdc1UyvcagDRFCLobs4GL_Erjs,1987
|
|
12
|
+
prettyplay/cache/store.py,sha256=tjwyTioi1zu-SNzBMMWPtThuJUWsh__BASvm15x5y5g,9168
|
|
13
|
+
prettyplay/cache/text.py,sha256=a1dd-ZrMIdykihxNcInC8a3zGkSis5dfxechxcfvvSo,1199
|
|
14
|
+
prettyplay/cache/.usages/addressing.md,sha256=9h2Fv-KIPQamvKmHE1PGLX0vKuqs6IQqJiKWCPxMibY,1327
|
|
15
|
+
prettyplay/cache/.usages/budgets.md,sha256=6mihnbXUNxZCKRYguofu5y1ied5ytJ8d2pxMgdxMPWE,889
|
|
16
|
+
prettyplay/cache/.usages/storage.md,sha256=QlOU8Z5lQ8R8jBwBklNula79ERxtkftt5iP-KC6Cnuo,1320
|
|
17
|
+
prettyplay/config/CODEMANIFEST,sha256=xTA3y3pQ1zbvRVvpDRbHT4ASIze9-nZDmHYhbaWPf28,7912
|
|
18
|
+
prettyplay/config/__init__.py,sha256=I9vREkOLZpoWJnk36OlrEgyCPmlgTgTBGTWSXG1mhPU,241
|
|
19
|
+
prettyplay/config/loader.py,sha256=MGHEDJg6CMMhrqIQXIEuO_C0Xsc8VCT6bwxyfnvtJCE,7065
|
|
20
|
+
prettyplay/config/models.py,sha256=hM8sJAxxHXn11Crx-3dZ0VpBWi8E6yh6by-iZPfSwa4,3658
|
|
21
|
+
prettyplay/config/.usages/configuration.md,sha256=dWmyTyrYAgtBD4_ytzKJIbRK0HNuZzqmAsZxTp7pj5c,3978
|
|
22
|
+
prettyplay/driver/CODEMANIFEST,sha256=GferPW_EXtC_-04vqqBGYaCktskfhAwvVjZvHK2Ac9o,9430
|
|
23
|
+
prettyplay/driver/__init__.py,sha256=AkT-oLEjL_xb9Xi-h_Qvx7oHzkeyiAgm4dowMmo3PKk,226
|
|
24
|
+
prettyplay/driver/page.py,sha256=lI1JkOh6KD0Lj-6z2dw8y-SgDzrTaDACTi0_Swukpzg,12365
|
|
25
|
+
prettyplay/driver/session.py,sha256=V-2nI_EKuxv2ygmGw8Pthqh2UW9qC8btQsP7oxlegfE,10506
|
|
26
|
+
prettyplay/driver/.usages/facade.md,sha256=ZVEmIBJY4LXSUmMr16PnQCtGs-fdr5dDWtHCSz1EqwI,3376
|
|
27
|
+
prettyplay/engine/CODEMANIFEST,sha256=I6ZX1sle9z58T_ncD4kJfUrYiFTGZ-1F-v5Wa_9-JsM,14059
|
|
28
|
+
prettyplay/engine/__init__.py,sha256=4TXAo9CJxkg1MgcE5IuuP8yFI980tPn4orEPf9ZUMeU,333
|
|
29
|
+
prettyplay/engine/classification.py,sha256=MGTgWKIHtH6DXPGZ2LGhqr2neYJ4j-av2SY2_5R0Mh4,2704
|
|
30
|
+
prettyplay/engine/execution.py,sha256=L8OmLw8oeoaOO2Jg_lL-9WuEvntO6qWsGp7eDDxvJcc,1076
|
|
31
|
+
prettyplay/engine/generator.py,sha256=K_VFJqWklCNt17dhXMFDsBE8p7FUvzlGS9a23HmZX50,14469
|
|
32
|
+
prettyplay/engine/healer.py,sha256=5lf8NEunNQY7499n2XmUf9m2zMy_05GiAhwqAFcfYko,5212
|
|
33
|
+
prettyplay/engine/text.py,sha256=OhS7oR1wbUfMYk74GKMV7AtWIN5BcZ9fxNgq1SRorOg,718
|
|
34
|
+
prettyplay/engine/.usages/generation.md,sha256=80rON1oqAAu2U--fJd2qDYcpmGmLVg2oAjW7KkS9Wmc,2865
|
|
35
|
+
prettyplay/engine/.usages/healing.md,sha256=-rV17c7udKUx1WTVJgm8XzkDuM42nwvNs2YVNDjLlmM,1591
|
|
36
|
+
prettyplay/failures/CODEMANIFEST,sha256=OAg5FA0Wz9wUMAYt058TiEsvEoTCb30G0USQFt92cPg,5998
|
|
37
|
+
prettyplay/failures/__init__.py,sha256=nUhcscgBJsPZroIabB8_pFJxAgBBU6DPHNzp85C1CzI,361
|
|
38
|
+
prettyplay/failures/errors.py,sha256=hJz8czDaClKpeY6pwZbxerZB0UrmkKVv3iCVAqtF6Zw,5222
|
|
39
|
+
prettyplay/failures/.usages/taxonomy.md,sha256=gpi8v4UEqtOnFrigvoswIOL3QnSBtKKs2uKUEBnQbUo,2483
|
|
40
|
+
prettyplay/llm/CODEMANIFEST,sha256=jhsBkhi8EXRLspX8Y1ErTLylVG09irz1SaRBY1Y7HWE,7235
|
|
41
|
+
prettyplay/llm/__init__.py,sha256=BwjZpXIc4K11GuVw4UDgQAgkihqb7kYSS7eIOGFlt8A,377
|
|
42
|
+
prettyplay/llm/_request.py,sha256=F-iLnGyEdlwL0PnqMFCRZukiW4kwv2DCFEHEEExc5KE,7528
|
|
43
|
+
prettyplay/llm/anthropic_provider.py,sha256=V-OPAk8uCcDOHq5vkfqZMm4ne8L0EP0GmaNALZuMFOE,8349
|
|
44
|
+
prettyplay/llm/models.py,sha256=26aM3-JJzkJj9t5PXRi9RbmXXyDwWKvwZb2bW8N8gyA,638
|
|
45
|
+
prettyplay/llm/openai_provider.py,sha256=rjG6UJi8ljQecjQpIHjpPz932NTpnXYQY90z_Ruy9x0,7420
|
|
46
|
+
prettyplay/llm/provider.py,sha256=jjtB5YVO1w5FyYMdY0pPFiQJ8JtgNlO3St2ymZjrHbs,4714
|
|
47
|
+
prettyplay/llm/.usages/classification.md,sha256=Ue-q8PZoJndKS0BIpea0iMTfZSCk9LCEhVPaOFyH0lA,994
|
|
48
|
+
prettyplay/llm/.usages/providers.md,sha256=EJ6lk-imG0WmOPq3wXprvVYYI-0S3P5w4GPkzBFhDzw,1540
|
|
49
|
+
prettyplay/reporting/CODEMANIFEST,sha256=D2zwoqvIg-ZXXzt_rozjFjvpd0LBPXtbiict-9P_akY,4153
|
|
50
|
+
prettyplay/reporting/__init__.py,sha256=_dmLcKn7WDDqBWNAHT0VTRHPZu2-nrMpfux60gASTU0,187
|
|
51
|
+
prettyplay/reporting/hooks.py,sha256=KqnmYgvy_2oZv3swSCCOL7hLkA5UI17cOaifqWSCgxU,1867
|
|
52
|
+
prettyplay/reporting/reporter.py,sha256=TlOdgCIrmUZCobxAhUzmq1qs2ycjiNMR9HOaiwfxFXM,2325
|
|
53
|
+
prettyplay/reporting/.usages/hooks.md,sha256=u5I0vExr-1CoNXUz7J7yVOfugAIhaKSFEYFS7Ou5NUg,2315
|
|
54
|
+
prettyplay-0.0.0.dist-info/licenses/LICENSE,sha256=3fteeL9wFLa2P6MykXx6RkApbO0N9NUrSs9rVH8gxnc,1493
|
|
55
|
+
prettyplay-0.0.0.dist-info/METADATA,sha256=N-jueIY0iyqXYHzNUCBNlKS-_L2FclcGJLQFwRdPfCM,9466
|
|
56
|
+
prettyplay-0.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
57
|
+
prettyplay-0.0.0.dist-info/top_level.txt,sha256=ZByeTYDEYyWHvSGaI4cQYCrh1oiDpK5bTfnwv_ogWUg,11
|
|
58
|
+
prettyplay-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, qarium
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prettyplay
|