qt-css-engine 0.1.2__tar.gz → 0.1.4__tar.gz

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.
Files changed (110) hide show
  1. qt_css_engine-0.1.4/.gitattributes +1 -0
  2. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/.gitignore +1 -0
  3. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/PKG-INFO +3 -3
  4. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/README.md +1 -1
  5. qt_css_engine-0.1.4/benchmarks/README.md +76 -0
  6. qt_css_engine-0.1.4/benchmarks/__init__.py +8 -0
  7. qt_css_engine-0.1.4/benchmarks/__main__.py +160 -0
  8. qt_css_engine-0.1.4/benchmarks/bench_attr_change.py +10 -0
  9. qt_css_engine-0.1.4/benchmarks/bench_attr_matching.py +25 -0
  10. qt_css_engine-0.1.4/benchmarks/bench_class_anim_frames.py +10 -0
  11. qt_css_engine-0.1.4/benchmarks/bench_class_change.py +10 -0
  12. qt_css_engine-0.1.4/benchmarks/bench_cold_matching.py +10 -0
  13. qt_css_engine-0.1.4/benchmarks/bench_hover_change.py +10 -0
  14. qt_css_engine-0.1.4/benchmarks/bench_hover_frames.py +10 -0
  15. qt_css_engine-0.1.4/benchmarks/bench_initial_eval.py +10 -0
  16. qt_css_engine-0.1.4/benchmarks/bench_resize_storm.py +10 -0
  17. qt_css_engine-0.1.4/benchmarks/bench_warm_matching.py +10 -0
  18. qt_css_engine-0.1.4/benchmarks/common.py +426 -0
  19. qt_css_engine-0.1.4/benchmarks/reporting.py +139 -0
  20. qt_css_engine-0.1.4/benchmarks/run.py +6 -0
  21. qt_css_engine-0.1.4/benchmarks/runner.py +158 -0
  22. qt_css_engine-0.1.4/benchmarks/workloads.py +244 -0
  23. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/demo/layout_test.py +40 -26
  24. qt_css_engine-0.1.4/demo/main.py +824 -0
  25. qt_css_engine-0.1.4/demo/styles.css +856 -0
  26. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/pyproject.toml +2 -0
  27. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/qt_css_engine/__init__.py +2 -2
  28. qt_css_engine-0.1.4/qt_css_engine/animation/base.py +23 -0
  29. qt_css_engine-0.1.4/qt_css_engine/animation/callbacks.py +20 -0
  30. qt_css_engine-0.1.4/qt_css_engine/animation/color.py +141 -0
  31. qt_css_engine-0.1.4/qt_css_engine/animation/delay.py +34 -0
  32. qt_css_engine-0.1.4/qt_css_engine/animation/factory.py +46 -0
  33. qt_css_engine-0.1.4/qt_css_engine/animation/numeric.py +261 -0
  34. qt_css_engine-0.1.4/qt_css_engine/animation/opacity.py +62 -0
  35. qt_css_engine-0.1.4/qt_css_engine/animation/shadow.py +106 -0
  36. qt_css_engine-0.1.4/qt_css_engine/animation/steps.py +8 -0
  37. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/qt_css_engine/constants.py +3 -7
  38. {qt_css_engine-0.1.2/qt_css_engine → qt_css_engine-0.1.4/qt_css_engine/css}/gradients.py +15 -11
  39. qt_css_engine-0.1.4/qt_css_engine/css/model.py +42 -0
  40. qt_css_engine-0.1.4/qt_css_engine/css/parser.py +255 -0
  41. qt_css_engine-0.1.4/qt_css_engine/css/properties.py +8 -0
  42. qt_css_engine-0.1.4/qt_css_engine/css/selector.py +18 -0
  43. qt_css_engine-0.1.4/qt_css_engine/css/shorthand.py +87 -0
  44. qt_css_engine-0.1.4/qt_css_engine/engine/__init__.py +5 -0
  45. qt_css_engine-0.1.4/qt_css_engine/engine/cascade.py +115 -0
  46. qt_css_engine-0.1.4/qt_css_engine/engine/evaluation.py +64 -0
  47. qt_css_engine-0.1.4/qt_css_engine/engine/evaluator.py +621 -0
  48. qt_css_engine-0.1.4/qt_css_engine/engine/event_router.py +94 -0
  49. qt_css_engine-0.1.4/qt_css_engine/engine/handlers/class_change.py +50 -0
  50. qt_css_engine-0.1.4/qt_css_engine/engine/handlers/clicked.py +52 -0
  51. qt_css_engine-0.1.4/qt_css_engine/engine/handlers/lifecycle.py +68 -0
  52. qt_css_engine-0.1.4/qt_css_engine/engine/handlers/parent_change.py +25 -0
  53. qt_css_engine-0.1.4/qt_css_engine/engine/handlers/polish.py +55 -0
  54. qt_css_engine-0.1.4/qt_css_engine/engine/handlers/reload.py +149 -0
  55. qt_css_engine-0.1.4/qt_css_engine/engine/handlers/window.py +54 -0
  56. qt_css_engine-0.1.4/qt_css_engine/engine/transition_engine.py +344 -0
  57. qt_css_engine-0.1.4/qt_css_engine/geometry/box_model.py +61 -0
  58. qt_css_engine-0.1.4/qt_css_engine/geometry/clamp.py +126 -0
  59. qt_css_engine-0.1.4/qt_css_engine/geometry/natural_size.py +86 -0
  60. qt_css_engine-0.1.4/qt_css_engine/matching/cache.py +74 -0
  61. qt_css_engine-0.1.4/qt_css_engine/matching/compiler.py +131 -0
  62. qt_css_engine-0.1.4/qt_css_engine/matching/index.py +67 -0
  63. qt_css_engine-0.1.4/qt_css_engine/matching/matcher.py +334 -0
  64. qt_css_engine-0.1.4/qt_css_engine/qt_compat/__init__.py +37 -0
  65. qt_css_engine-0.1.4/qt_css_engine/state/pseudo.py +35 -0
  66. qt_css_engine-0.1.4/qt_css_engine/state/store.py +39 -0
  67. qt_css_engine-0.1.4/qt_css_engine/state/suppress.py +33 -0
  68. qt_css_engine-0.1.4/qt_css_engine/state/widget_state.py +192 -0
  69. qt_css_engine-0.1.4/qt_css_engine/style/cursor.py +17 -0
  70. qt_css_engine-0.1.4/qt_css_engine/style/effects.py +57 -0
  71. qt_css_engine-0.1.4/qt_css_engine/style/writer.py +127 -0
  72. qt_css_engine-0.1.4/qt_css_engine/utils/color.py +178 -0
  73. qt_css_engine-0.1.4/qt_css_engine/utils/easing.py +83 -0
  74. qt_css_engine-0.1.4/qt_css_engine/utils/parsing.py +33 -0
  75. qt_css_engine-0.1.4/qt_css_engine/utils/qt_helpers.py +17 -0
  76. qt_css_engine-0.1.4/tests/conftest.py +60 -0
  77. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/tests/test_anim.py +668 -212
  78. qt_css_engine-0.1.4/tests/test_attr_selectors.py +278 -0
  79. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/tests/test_css_parser.py +133 -3
  80. qt_css_engine-0.1.4/tests/test_edgecases.py +1755 -0
  81. qt_css_engine-0.1.4/tests/test_edgecases_extra.py +702 -0
  82. qt_css_engine-0.1.4/tests/test_edgecases_final.py +520 -0
  83. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/tests/test_engine_coverage.py +404 -97
  84. qt_css_engine-0.1.4/tests/test_fixes.py +321 -0
  85. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/tests/test_gradients.py +1 -1
  86. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/tests/test_interactions.py +159 -17
  87. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/tests/test_layouts.py +2 -2
  88. qt_css_engine-0.1.4/tests/test_radius_resize.py +123 -0
  89. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/tests/test_size_layout.py +153 -103
  90. qt_css_engine-0.1.4/tests/test_specificity.py +197 -0
  91. qt_css_engine-0.1.2/demo/main.py +0 -438
  92. qt_css_engine-0.1.2/demo/styles.css +0 -364
  93. qt_css_engine-0.1.2/qt_css_engine/css_parser.py +0 -397
  94. qt_css_engine-0.1.2/qt_css_engine/engine.py +0 -1479
  95. qt_css_engine-0.1.2/qt_css_engine/handlers.py +0 -495
  96. qt_css_engine-0.1.2/qt_css_engine/qt_compat/__init__.py +0 -35
  97. qt_css_engine-0.1.2/qt_css_engine/types.py +0 -89
  98. qt_css_engine-0.1.2/qt_css_engine/utils.py +0 -481
  99. qt_css_engine-0.1.2/tests/conftest.py +0 -14
  100. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/.github/CODE_OF_CONDUCT.md +0 -0
  101. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/.github/CONTRIBUTING.md +0 -0
  102. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/.github/workflows/pr-check.yml +0 -0
  103. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/.github/workflows/publish.yml +0 -0
  104. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/.github/workflows/test-publish.yml +0 -0
  105. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/LICENSE.md +0 -0
  106. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/qt_css_engine/py.typed +0 -0
  107. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/qt_css_engine/qt_compat/QtCore.py +0 -0
  108. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/qt_css_engine/qt_compat/QtGui.py +0 -0
  109. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/qt_css_engine/qt_compat/QtWidgets.py +0 -0
  110. {qt_css_engine-0.1.2 → qt_css_engine-0.1.4}/qt_css_engine/qt_compat/_api.py +0 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -8,6 +8,7 @@ wheels/
8
8
  .venv
9
9
  .pytest_cache
10
10
  .ruff_cache
11
+ .benchmarks/
11
12
  .vscode
12
13
  .python-version
13
14
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: qt-css-engine
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: CSS transition animations for PyQt6/PySide6 — extends static QSS with transition and other properties
5
5
  Project-URL: Homepage, https://github.com/Video-Nomad/qt-css-engine
6
6
  Project-URL: Repository, https://github.com/Video-Nomad/qt-css-engine
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  A CSS animation engine for PyQt6/PySide6 that extends Qt's static stylesheet system (QSS) with dynamic CSS transitions and extra properties like `box-shadow`, `opacity` and CSS gradients. Qt's stylesheet engine has no concept of time or interpolation — this project implements an out-of-band animation system that intercepts `transition:` declarations from a stylesheet, tracks widget pseudo-states (hover, pressed, focus), and drives smooth property animations via Qt's animation framework. Dynamic class change is also supported so `.btn` -> `.btn.active` -> `.btn.other-state` will animate based on the `transition` property.
29
29
 
30
- All that is required is to install `TransitionEngine` as an event filter and it will take care of the rest.
30
+ All that is required is to install `TransitionEngine` as an event filter and it will take care of the rest. It can be installed directly on a QApplication or any child QWidget.
31
31
 
32
32
  CSS Hot reload is supported via `TransitionEngine.reload_rules(new_rules)`
33
33
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  A CSS animation engine for PyQt6/PySide6 that extends Qt's static stylesheet system (QSS) with dynamic CSS transitions and extra properties like `box-shadow`, `opacity` and CSS gradients. Qt's stylesheet engine has no concept of time or interpolation — this project implements an out-of-band animation system that intercepts `transition:` declarations from a stylesheet, tracks widget pseudo-states (hover, pressed, focus), and drives smooth property animations via Qt's animation framework. Dynamic class change is also supported so `.btn` -> `.btn.active` -> `.btn.other-state` will animate based on the `transition` property.
4
4
 
5
- All that is required is to install `TransitionEngine` as an event filter and it will take care of the rest.
5
+ All that is required is to install `TransitionEngine` as an event filter and it will take care of the rest. It can be installed directly on a QApplication or any child QWidget.
6
6
 
7
7
  CSS Hot reload is supported via `TransitionEngine.reload_rules(new_rules)`
8
8
 
@@ -0,0 +1,76 @@
1
+ # Performance comparisons
2
+
3
+ Run from the repository root in PowerShell. Pin the binding, then save a baseline
4
+ **using this version of the benchmarks before changing the engine**:
5
+
6
+ ```powershell
7
+ $env:QT_API = "pyqt6" # or pyside6; keep it the same for both runs
8
+ uv run python -m benchmarks --output .benchmarks/before.json
9
+
10
+ # Make the engine change, then run:
11
+ uv run python -m benchmarks --compare .benchmarks/before.json --output .benchmarks/after.json
12
+ ```
13
+
14
+ Negative median changes mean faster; positive changes mean slower. Compare the
15
+ sample spread too. Small changes within the run-to-run variation are inconclusive.
16
+ For a closer look, use `--repeat 15 --warmup 3` on **both** runs and repeat the
17
+ before/after experiment. Use the same machine, power mode, dependencies and idle
18
+ background workload. Run timings without other test suites or profilers running.
19
+
20
+ `--filter class` selects a subset; `--list` lists keys. A filtered run can compare
21
+ against a full baseline. `--repeat 1 --warmup 0` is a smoke check, not a reliable
22
+ performance measurement. `--json` emits a complete JSON document on stdout;
23
+ progress and Qt diagnostics go to stderr. `.benchmarks/` is ignored by Git.
24
+
25
+ Display a saved report as the usual results table without running benchmarks:
26
+
27
+ ```powershell
28
+ uv run python -m benchmarks --show .benchmarks/after.json
29
+ ```
30
+
31
+ ## Measurement boundaries
32
+
33
+ The fixture has 321 widgets and 458 rules (498 for attribute cases). It includes
34
+ 40 buttons, nested wrapper/container branches and radius-styled noise labels.
35
+ The cleaned static QSS is applied to the root, alongside the engine rules, so
36
+ style writes and repolishing exercise Qt's stylesheet machinery too.
37
+
38
+ | Key | Timed work |
39
+ | --- | --- |
40
+ | `cold` | One matching pass over all 321 widgets with empty widget/identity caches; rule compilation/index construction and cache clearing are excluded. |
41
+ | `warm` | Matching with primed caches; 100 passes per sample, reported as milliseconds per **one 321-widget pass**. |
42
+ | `attr_cold`, `attr_warm` | Matching plus live attribute predicates, as used by the cascade. Half the 40 buttons have `active=true`. Warm samples use 100 passes and the same per-pass normalization. |
43
+ | `initial`, `attr_initial` | New engine/index construction, 321 delivered Polish events, and queued engine work until settled. Each sample has a fresh tree with static QSS already installed. CSS parsing, tree creation, native first show, and destruction are excluded. |
44
+ | `class_change`, `attr` | A synchronous burst of 40 property changes through Qt's event filter, including repolish and creation of 200 animations. Deferred flushes, layout, painting and animation ticks are excluded. |
45
+ | `hover` | 40 delivered `QHoverEvent` enters through the event filter, including creation of 200 animations. Like the property-change cases, deferred work is excluded. |
46
+ | `class_anim`, `hover_anim` | 15 synthetic frames at 20–300 ms for 40 × 5 animations, including deferred style/layout work and final completion callbacks. Trigger/setup work is excluded. Results are per **15-frame sequence**, not FPS or per-frame latency. |
47
+ | `resize` | One actual resize on each of 100 labels, from 120×30 to 122×34, plus queued clamp/style work. Layouts are frozen beforehand so they cannot undo or multiply the resizes. Each radius must change from 15 to 17 px. |
48
+
49
+ These are fixed workload regression benchmarks, not exhaustive engine coverage.
50
+ They do not currently measure parsing, hot reload, reparenting, effects, or
51
+ real-time paint/compositor throughput. Class and attribute scenarios also have
52
+ different rule counts; compare each scenario to itself across engine revisions.
53
+
54
+ ## Repeatability and correctness
55
+
56
+ - Each scenario runs in a fresh Python process. `PYTHONHASHSEED` defaults to `0`
57
+ for the worker processes, or preserves your explicitly supplied seed.
58
+ - Setup, teardown, correctness checks and garbage collection are outside timing.
59
+ Cyclic GC is disabled during each timed sample and its previous state restored.
60
+ Samples have fresh trees/engines; warm matcher caches are deliberately primed.
61
+ - Animation clocks are paused at time zero before processing any queued events.
62
+ Every synthetic frame seeks all 200 animations explicitly; the wall clock cannot
63
+ advance them. The last frame includes completion, so write counts need not be
64
+ exactly 15 × 40. Write counts are diagnostics from a separate untimed sample.
65
+ - Windows use the native Qt platform with `WA_DontShowOnScreen`, avoiding desktop
66
+ focus/mouse interference. The CLI rejects `offscreen` and `minimal` platforms.
67
+ No global Qt message handler suppresses errors.
68
+ - Assertions verify fixture sizes, matcher results against an uncached scan,
69
+ attribute hits/misses, five running properties per button, final animation
70
+ values and resize clamp changes. Errors fail the run; partial suites are never
71
+ saved as successful baselines. Run Python normally, without `-O`.
72
+ - JSON retains every sample, summary statistics, warmup counts, workload sizes,
73
+ source revision/dirty flag, engine and benchmark source hashes, Python/Qt/binding
74
+ versions, platform, style, font, DPI and hash seed. Comparisons reject changed
75
+ benchmark code, environment or sampling settings. Engine code/revision changes
76
+ are expected. Metadata cannot detect every source of system noise.
@@ -0,0 +1,8 @@
1
+ """Benchmark suite for qt_css_engine.
2
+
3
+ Run with:
4
+ uv run python -m benchmarks
5
+ uv run python -m benchmarks --help
6
+ uv run python -m benchmarks --filter cold
7
+ uv run python -m benchmarks --json
8
+ """
@@ -0,0 +1,160 @@
1
+ """Run each scenario in a fresh Python process and save comparable results."""
2
+
3
+ import argparse
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ from benchmarks import (
12
+ bench_attr_change,
13
+ bench_attr_matching,
14
+ bench_class_anim_frames,
15
+ bench_class_change,
16
+ bench_cold_matching,
17
+ bench_hover_change,
18
+ bench_hover_frames,
19
+ bench_initial_eval,
20
+ bench_resize_storm,
21
+ bench_warm_matching,
22
+ )
23
+ from benchmarks.reporting import Report, comparison_table, environment, read_report, report_json
24
+ from benchmarks.runner import BenchResult, format_table
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class BenchEntry:
29
+ key: str
30
+ name: str
31
+ benchmark: Callable[..., BenchResult]
32
+
33
+
34
+ # Order for baseline table
35
+ ALL_BENCHES: list[BenchEntry] = [
36
+ BenchEntry("cold", bench_cold_matching.NAME, bench_cold_matching.benchmark),
37
+ BenchEntry("warm", bench_warm_matching.NAME, bench_warm_matching.benchmark),
38
+ BenchEntry("initial", bench_initial_eval.NAME, bench_initial_eval.benchmark),
39
+ BenchEntry("attr_cold", bench_attr_matching.NAME_COLD, bench_attr_matching.benchmark_cold),
40
+ BenchEntry("attr_warm", bench_attr_matching.NAME_WARM, bench_attr_matching.benchmark_warm),
41
+ BenchEntry("attr_initial", bench_attr_matching.NAME_INITIAL, bench_attr_matching.benchmark_initial),
42
+ BenchEntry("class_change", bench_class_change.NAME, bench_class_change.benchmark),
43
+ BenchEntry("attr", bench_attr_change.NAME, bench_attr_change.benchmark),
44
+ BenchEntry("class_anim", bench_class_anim_frames.NAME, bench_class_anim_frames.benchmark),
45
+ BenchEntry("hover", bench_hover_change.NAME, bench_hover_change.benchmark),
46
+ BenchEntry("hover_anim", bench_hover_frames.NAME, bench_hover_frames.benchmark),
47
+ BenchEntry("resize", bench_resize_storm.NAME, bench_resize_storm.benchmark),
48
+ ]
49
+
50
+
51
+ def positive(value: str) -> int:
52
+ number = int(value)
53
+ if number < 1:
54
+ raise argparse.ArgumentTypeError("must be at least 1")
55
+ return number
56
+
57
+
58
+ def nonnegative(value: str) -> int:
59
+ number = int(value)
60
+ if number < 0:
61
+ raise argparse.ArgumentTypeError("must be at least 0")
62
+ return number
63
+
64
+
65
+ def main() -> None:
66
+ parser = argparse.ArgumentParser(description=__doc__)
67
+ parser.add_argument("--list", action="store_true", help="list scenarios and exit")
68
+ parser.add_argument("--filter", default="", help="case-insensitive substring of key or name")
69
+ parser.add_argument("--json", action="store_true", help="print only a JSON report to stdout")
70
+ parser.add_argument("--output", type=Path, help="save a JSON report")
71
+ parser.add_argument("--show", type=Path, help="print a saved JSON report as a table and exit")
72
+ parser.add_argument("--compare", type=Path, help="compare medians with a saved report")
73
+ parser.add_argument("--repeat", type=positive, help="measured samples per scenario")
74
+ parser.add_argument("--warmup", type=nonnegative, help="discarded warmup samples")
75
+ parser.add_argument("-q", "--quiet", action="store_true", help="suppress progress on stderr")
76
+ parser.add_argument("--worker", choices=[entry.key for entry in ALL_BENCHES], help=argparse.SUPPRESS)
77
+ args = parser.parse_args()
78
+ if args.list:
79
+ for entry in ALL_BENCHES:
80
+ print(f"{entry.key:14} {entry.name}")
81
+ return
82
+ if args.show:
83
+ try:
84
+ report = read_report(args.show)
85
+ except (OSError, ValueError, KeyError, TypeError) as error:
86
+ parser.error(f"cannot read report: {error}")
87
+ print(format_table(list(report.results.values())))
88
+ print("\nAll timings are milliseconds per workload; writes are from a separate untimed sample.")
89
+ return
90
+ if os.environ.get("QT_QPA_PLATFORM", "").lower() in {"offscreen", "minimal"}:
91
+ parser.error("use the native Qt platform; remove QT_QPA_PLATFORM=offscreen/minimal")
92
+ if args.worker:
93
+ entry = next(entry for entry in ALL_BENCHES if entry.key == args.worker)
94
+ options: dict[str, int] = {}
95
+ if args.repeat is not None:
96
+ options["runs"] = args.repeat
97
+ if args.warmup is not None:
98
+ options["warmup"] = args.warmup
99
+ result = entry.benchmark(**options)
100
+ print(report_json(Report(environment(), {entry.key: result})))
101
+ return
102
+ selected = [entry for entry in ALL_BENCHES if args.filter.lower() in f"{entry.key} {entry.name}".lower()]
103
+ if not selected:
104
+ parser.error(f"no scenarios match {args.filter!r}")
105
+ try:
106
+ baseline = read_report(args.compare) if args.compare else None
107
+ except (OSError, ValueError, KeyError, TypeError) as error:
108
+ parser.error(f"cannot read baseline: {error}")
109
+ report = Report({}, {})
110
+ failures: list[str] = []
111
+ env = os.environ.copy()
112
+ env.setdefault("PYTHONHASHSEED", "0")
113
+ for entry in selected:
114
+ if not args.quiet:
115
+ print(f"Running {entry.name} ...", file=sys.stderr, flush=True)
116
+ command = [sys.executable, "-m", "benchmarks", "--worker", entry.key]
117
+ if args.repeat is not None:
118
+ command += ["--repeat", str(args.repeat)]
119
+ if args.warmup is not None:
120
+ command += ["--warmup", str(args.warmup)]
121
+ process = subprocess.run(
122
+ command, cwd=Path(__file__).resolve().parents[1], env=env, capture_output=True, text=True
123
+ )
124
+ if process.stderr:
125
+ print(process.stderr, file=sys.stderr, end="")
126
+ if process.returncode:
127
+ failures.append(entry.key)
128
+ continue
129
+ child = Report.from_json(process.stdout)
130
+ if report.metadata and any(
131
+ report.metadata.get(key) != value for key, value in child.metadata.items() if key != "recorded_at"
132
+ ):
133
+ parser.exit(1, "Code or environment changed during the run; no report saved.\n")
134
+ report.metadata = child.metadata
135
+ report.results.update(child.results)
136
+ if failures:
137
+ # Never save a partial run as a successful baseline.
138
+ parser.exit(1, f"Failed scenarios: {', '.join(failures)}; no report saved.\n")
139
+ comparison = None
140
+ if baseline:
141
+ try:
142
+ comparison = comparison_table(baseline, report)
143
+ except ValueError as error:
144
+ parser.error(str(error))
145
+ if args.output:
146
+ args.output.parent.mkdir(parents=True, exist_ok=True)
147
+ args.output.write_text(report_json(report) + "\n", encoding="utf-8")
148
+ if args.json:
149
+ print(report_json(report))
150
+ if comparison:
151
+ print(comparison, file=sys.stderr)
152
+ else:
153
+ print(format_table(list(report.results.values())))
154
+ print("\nAll timings are milliseconds per workload; writes are from a separate untimed sample.")
155
+ if comparison:
156
+ print("\n" + comparison)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
@@ -0,0 +1,10 @@
1
+ """Attr change. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import change
5
+
6
+ NAME = "Attr change"
7
+
8
+
9
+ def benchmark(*, warmup: int = 2, runs: int = 7) -> BenchResult:
10
+ return change(NAME, kind="attr", warmup=warmup, runs=runs)
@@ -0,0 +1,25 @@
1
+ """Matching includes the live attribute predicates used by the cascade."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import initial, matching
5
+
6
+ NAME_COLD = "Cold attr matching"
7
+ NAME_WARM = "Warm attr matching"
8
+ NAME_INITIAL = "Initial attr evaluation"
9
+ NAME = NAME_COLD
10
+
11
+
12
+ def benchmark_cold(*, warmup: int = 2, runs: int = 7) -> BenchResult:
13
+ return matching(NAME_COLD, cold=True, attrs=True, warmup=warmup, runs=runs)
14
+
15
+
16
+ def benchmark_warm(*, warmup: int = 3, runs: int = 15) -> BenchResult:
17
+ return matching(NAME_WARM, cold=False, attrs=True, warmup=warmup, runs=runs)
18
+
19
+
20
+ def benchmark_initial(*, warmup: int = 2, runs: int = 7) -> BenchResult:
21
+ return initial(NAME_INITIAL, attrs=True, warmup=warmup, runs=runs)
22
+
23
+
24
+ def benchmark(*, warmup: int = 2, runs: int = 7) -> BenchResult:
25
+ return benchmark_cold(warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Class-animation frames. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import frames
5
+
6
+ NAME = "Class-animation frames"
7
+
8
+
9
+ def benchmark(*, warmup: int = 1, runs: int = 5) -> BenchResult:
10
+ return frames(NAME, kind="class", warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Class change. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import change
5
+
6
+ NAME = "Class change"
7
+
8
+
9
+ def benchmark(*, warmup: int = 2, runs: int = 7) -> BenchResult:
10
+ return change(NAME, kind="class", warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Cold rule matching. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import matching
5
+
6
+ NAME = "Cold rule matching"
7
+
8
+
9
+ def benchmark(*, warmup: int = 2, runs: int = 7) -> BenchResult:
10
+ return matching(NAME, cold=True, attrs=False, warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Hover event. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import change
5
+
6
+ NAME = "Hover event"
7
+
8
+
9
+ def benchmark(*, warmup: int = 2, runs: int = 7) -> BenchResult:
10
+ return change(NAME, kind="hover", warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Hover-animation frames. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import frames
5
+
6
+ NAME = "Hover-animation frames"
7
+
8
+
9
+ def benchmark(*, warmup: int = 1, runs: int = 5) -> BenchResult:
10
+ return frames(NAME, kind="hover", warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Initial evaluation. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import initial
5
+
6
+ NAME = "Initial evaluation"
7
+
8
+
9
+ def benchmark(*, warmup: int = 2, runs: int = 7) -> BenchResult:
10
+ return initial(NAME, attrs=False, warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Resize storm. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import resize
5
+
6
+ NAME = "Resize storm"
7
+
8
+
9
+ def benchmark(*, warmup: int = 2, runs: int = 7) -> BenchResult:
10
+ return resize(NAME, warmup=warmup, runs=runs)
@@ -0,0 +1,10 @@
1
+ """Warm cached matching. See benchmarks/README.md for the timing boundary."""
2
+
3
+ from benchmarks.runner import BenchResult
4
+ from benchmarks.workloads import matching
5
+
6
+ NAME = "Warm cached matching"
7
+
8
+
9
+ def benchmark(*, warmup: int = 3, runs: int = 15) -> BenchResult:
10
+ return matching(NAME, cold=False, attrs=False, warmup=warmup, runs=runs)