pytest-async-benchmark 0.1.1__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.
@@ -0,0 +1,31 @@
1
+ """pytest-async-benchmark: Modern pytest benchmarking for async code. ๐Ÿš€"""
2
+
3
+ __version__ = "0.1.1"
4
+ __author__ = "Mihai Farcas"
5
+ __email__ = "contact@mihai.ltd"
6
+
7
+ from .analytics import benchmark_summary, compare_benchmarks, performance_grade
8
+ from .comparison import (
9
+ BenchmarkComparator,
10
+ BenchmarkScenario,
11
+ a_vs_b_comparison,
12
+ quick_compare,
13
+ )
14
+ from .display import display_comparison_table, format_speedup, format_time
15
+ from .plugin import async_benchmark
16
+ from .runner import AsyncBenchmarkRunner
17
+
18
+ __all__ = [
19
+ "async_benchmark",
20
+ "AsyncBenchmarkRunner",
21
+ "BenchmarkComparator",
22
+ "BenchmarkScenario",
23
+ "quick_compare",
24
+ "a_vs_b_comparison",
25
+ "display_comparison_table",
26
+ "format_time",
27
+ "format_speedup",
28
+ "compare_benchmarks",
29
+ "performance_grade",
30
+ "benchmark_summary",
31
+ ]
@@ -0,0 +1,49 @@
1
+ """Additional utilities for pytest-async-benchmark."""
2
+
3
+ import statistics
4
+ from typing import Any
5
+
6
+
7
+ def compare_benchmarks(
8
+ result1: dict[str, Any], result2: dict[str, Any]
9
+ ) -> dict[str, float]:
10
+ """Compare two benchmark results and return relative performance metrics."""
11
+ return {
12
+ "mean_ratio": result2["mean"] / result1["mean"],
13
+ "min_ratio": result2["min"] / result1["min"],
14
+ "max_ratio": result2["max"] / result1["max"],
15
+ "speedup": result1["mean"] / result2["mean"],
16
+ "relative_stddev": (result2["stddev"] - result1["stddev"]) / result1["stddev"],
17
+ }
18
+
19
+
20
+ def benchmark_summary(results: list[dict[str, Any]]) -> dict[str, Any]:
21
+ """Create a summary of multiple benchmark results."""
22
+ all_means = [r["mean"] for r in results]
23
+ all_stddevs = [r["stddev"] for r in results]
24
+
25
+ return {
26
+ "total_tests": len(results),
27
+ "fastest_mean": min(all_means),
28
+ "slowest_mean": max(all_means),
29
+ "average_mean": statistics.mean(all_means),
30
+ "mean_variation": statistics.stdev(all_means) if len(all_means) > 1 else 0,
31
+ "stability_score": 1
32
+ - (statistics.mean(all_stddevs) / statistics.mean(all_means)),
33
+ }
34
+
35
+
36
+ def performance_grade(result: dict[str, Any], thresholds: dict[str, float]) -> str:
37
+ """Grade performance based on configurable thresholds."""
38
+ mean_time = result["mean"]
39
+
40
+ if mean_time <= thresholds.get("excellent", 0.001):
41
+ return "A+"
42
+ elif mean_time <= thresholds.get("good", 0.005):
43
+ return "A"
44
+ elif mean_time <= thresholds.get("acceptable", 0.01):
45
+ return "B"
46
+ elif mean_time <= thresholds.get("slow", 0.05):
47
+ return "C"
48
+ else:
49
+ return "D"
@@ -0,0 +1,241 @@
1
+ """
2
+ Comparison utilities for pytest-async-benchmark.
3
+
4
+ This module provides high-level utilities for comparing multiple benchmark results
5
+ and displaying them in various formats.
6
+ """
7
+
8
+ from collections.abc import Awaitable
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable, Optional
11
+
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.text import Text
15
+
16
+ from .analytics import compare_benchmarks, performance_grade
17
+ from .display import display_comparison_table, format_speedup, format_time
18
+ from .runner import AsyncBenchmarkRunner
19
+
20
+
21
+ @dataclass
22
+ class BenchmarkScenario:
23
+ """Represents a single benchmark scenario."""
24
+
25
+ name: str
26
+ func: Callable[[], Awaitable[Any]]
27
+ rounds: int = 5
28
+ iterations: int = 10
29
+ description: Optional[str] = None
30
+
31
+
32
+ class BenchmarkComparator:
33
+ """High-level utility for comparing multiple benchmarks."""
34
+
35
+ def __init__(self, console: Optional[Console] = None):
36
+ self.console = console or Console()
37
+ self.runner = AsyncBenchmarkRunner()
38
+ self.results: list[dict[str, Any]] = []
39
+
40
+ async def add_scenario(
41
+ self,
42
+ name: str,
43
+ func: Callable[[], Awaitable[Any]],
44
+ rounds: int = 5,
45
+ iterations: int = 10,
46
+ description: Optional[str] = None,
47
+ ) -> dict[str, Any]:
48
+ """Add and run a benchmark scenario."""
49
+ scenario = BenchmarkScenario(name, func, rounds, iterations, description)
50
+
51
+ self.console.print(f"๐Ÿƒ Running scenario: [cyan]{name}[/cyan]")
52
+ if description:
53
+ self.console.print(f" {description}")
54
+
55
+ runner = AsyncBenchmarkRunner(rounds=rounds, iterations=iterations)
56
+ result = await runner.run(func)
57
+
58
+ scenario_result = {
59
+ "name": name,
60
+ "result": result,
61
+ "description": description,
62
+ "scenario": scenario,
63
+ }
64
+
65
+ self.results.append(scenario_result)
66
+ return scenario_result
67
+
68
+ def display_all_results(self, title: str = "๐Ÿ“Š Benchmark Results Summary"):
69
+ """Display all collected results in a summary table."""
70
+ if not self.results:
71
+ self.console.print("โŒ No benchmark results to display")
72
+ return
73
+
74
+ display_comparison_table(self.results, title=title, console=self.console)
75
+
76
+ def display_pairwise_comparison(
77
+ self, name1: str, name2: str, title: Optional[str] = None
78
+ ):
79
+ """Display a detailed comparison between two specific scenarios."""
80
+ result1 = next((r for r in self.results if r["name"] == name1), None)
81
+ result2 = next((r for r in self.results if r["name"] == name2), None)
82
+
83
+ if not result1 or not result2:
84
+ self.console.print(f"โŒ Could not find results for '{name1}' or '{name2}'")
85
+ return
86
+
87
+ comparison_title = title or f"โšก {name1} vs {name2}"
88
+
89
+ display_comparison_table(
90
+ [result1, result2], title=comparison_title, console=self.console
91
+ )
92
+
93
+ comparison = compare_benchmarks(result1["result"], result2["result"])
94
+
95
+ self.console.print("\n๐Ÿ“ˆ Detailed Analysis:")
96
+ analysis_text = Text()
97
+
98
+ speedup = comparison["speedup"]
99
+ if speedup > 1.2:
100
+ analysis_text.append(f"๐Ÿš€ {name2} is significantly faster ", style="green")
101
+ analysis_text.append(f"({format_speedup(speedup)})", style="bold green")
102
+ elif speedup < 0.8:
103
+ analysis_text.append(f"๐ŸŒ {name1} is significantly faster ", style="red")
104
+ analysis_text.append(f"({format_speedup(1 / speedup)})", style="bold red")
105
+ else:
106
+ analysis_text.append(
107
+ "โš–๏ธ Both scenarios have similar performance", style="yellow"
108
+ )
109
+
110
+ self.console.print(Panel(analysis_text, title="Performance Summary"))
111
+
112
+ def display_performance_grades(self, thresholds: Optional[dict[str, float]] = None):
113
+ """Display performance grades for all scenarios."""
114
+ if not self.results:
115
+ self.console.print("โŒ No results to grade")
116
+ return
117
+
118
+ default_thresholds = {
119
+ "excellent": 0.001,
120
+ "good": 0.005,
121
+ "acceptable": 0.01,
122
+ "slow": 0.05,
123
+ }
124
+ thresholds = thresholds or default_thresholds
125
+
126
+ from rich.table import Table
127
+
128
+ table = Table(title="๐Ÿ† Performance Grades")
129
+ table.add_column("Scenario", style="cyan")
130
+ table.add_column("Mean Time", style="yellow", justify="right")
131
+ table.add_column("Grade", style="green", justify="center")
132
+ table.add_column("Status", justify="center")
133
+
134
+ for result in self.results:
135
+ grade = performance_grade(result["result"], thresholds)
136
+ mean_time = format_time(result["result"]["mean"])
137
+
138
+ status = {"A+": "๐Ÿ†", "A": "๐Ÿฅ‡", "B": "๐Ÿฅˆ", "C": "๐Ÿฅ‰", "D": "๐ŸŒ"}.get(
139
+ grade, "โ“"
140
+ )
141
+
142
+ grade_style = {
143
+ "A+": "bold green",
144
+ "A": "green",
145
+ "B": "yellow",
146
+ "C": "orange3",
147
+ "D": "red",
148
+ }.get(grade, "white")
149
+
150
+ table.add_row(
151
+ result["name"],
152
+ mean_time,
153
+ f"[{grade_style}]{grade}[/{grade_style}]",
154
+ status,
155
+ )
156
+
157
+ self.console.print(table)
158
+
159
+ def get_fastest_scenario(self) -> Optional[dict[str, Any]]:
160
+ """Get the scenario with the fastest mean time."""
161
+ if not self.results:
162
+ return None
163
+
164
+ return min(self.results, key=lambda r: r["result"]["mean"])
165
+
166
+ def get_most_stable_scenario(self) -> Optional[dict[str, Any]]:
167
+ """Get the scenario with the lowest standard deviation."""
168
+ if not self.results:
169
+ return None
170
+
171
+ return min(self.results, key=lambda r: r["result"]["stddev"])
172
+
173
+ def export_results(self) -> list[dict[str, Any]]:
174
+ """Export all results for external analysis."""
175
+ return [
176
+ {
177
+ "name": r["name"],
178
+ "description": r["description"],
179
+ "mean": r["result"]["mean"],
180
+ "min": r["result"]["min"],
181
+ "max": r["result"]["max"],
182
+ "median": r["result"]["median"],
183
+ "stddev": r["result"]["stddev"],
184
+ "rounds": r["result"]["rounds"],
185
+ "iterations": r["result"]["iterations"],
186
+ }
187
+ for r in self.results
188
+ ]
189
+
190
+ def clear_results(self):
191
+ """Clear all stored results."""
192
+ self.results.clear()
193
+
194
+
195
+ async def quick_compare(
196
+ scenarios: list[BenchmarkScenario],
197
+ title: str = "๐Ÿš€ Quick Benchmark Comparison",
198
+ console: Optional[Console] = None,
199
+ ) -> BenchmarkComparator:
200
+ """Run a quick comparison of multiple scenarios."""
201
+ comparator = BenchmarkComparator(console)
202
+
203
+ comparator.console.print(f"\n[bold blue]{title}[/bold blue]")
204
+ comparator.console.print("=" * 50)
205
+
206
+ for scenario in scenarios:
207
+ await comparator.add_scenario(
208
+ scenario.name,
209
+ scenario.func,
210
+ scenario.rounds,
211
+ scenario.iterations,
212
+ scenario.description,
213
+ )
214
+
215
+ comparator.display_all_results(title)
216
+ comparator.display_performance_grades()
217
+
218
+ return comparator
219
+
220
+
221
+ async def a_vs_b_comparison(
222
+ name_a: str,
223
+ func_a: Callable[[], Awaitable[Any]],
224
+ name_b: str,
225
+ func_b: Callable[[], Awaitable[Any]],
226
+ rounds: int = 10,
227
+ iterations: int = 50,
228
+ console: Optional[Console] = None,
229
+ ) -> BenchmarkComparator:
230
+ """Quick A vs B comparison utility."""
231
+ comparator = BenchmarkComparator(console)
232
+
233
+ comparator.console.print(f"\n[bold magenta]โš”๏ธ {name_a} vs {name_b}[/bold magenta]")
234
+ comparator.console.print("=" * 50)
235
+
236
+ await comparator.add_scenario(name_a, func_a, rounds, iterations)
237
+ await comparator.add_scenario(name_b, func_b, rounds, iterations)
238
+
239
+ comparator.display_pairwise_comparison(name_a, name_b)
240
+
241
+ return comparator
@@ -0,0 +1,169 @@
1
+ """Display utilities for async benchmarking."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+
9
+ def format_time(seconds: float) -> str:
10
+ """Format time duration in human-readable format."""
11
+ if seconds >= 1.0:
12
+ return f"{seconds:.3f}s"
13
+ elif seconds >= 0.001:
14
+ return f"{seconds * 1000:.3f}ms"
15
+ elif seconds >= 0.000001:
16
+ return f"{seconds * 1000000:.3f}ฮผs"
17
+ else:
18
+ return f"{seconds * 1000000000:.3f}ns"
19
+
20
+
21
+ def format_percentage(value: float) -> str:
22
+ """Format percentage with appropriate styling."""
23
+ if value > 0:
24
+ return f"+{value:.1f}%"
25
+ else:
26
+ return f"{value:.1f}%"
27
+
28
+
29
+ def format_speedup(ratio: float) -> str:
30
+ """Format speedup ratio with descriptive text."""
31
+ if ratio > 1:
32
+ return f"{ratio:.2f}x faster"
33
+ elif ratio < 1:
34
+ return f"{1 / ratio:.2f}x slower"
35
+ else:
36
+ return "same speed"
37
+
38
+
39
+ def display_comparison_table(
40
+ comparisons: list[dict[str, Any]],
41
+ title: str = "๐Ÿš€ Async Benchmark Comparison",
42
+ console: Optional[Console] = None,
43
+ ) -> None:
44
+ """Display a comparison table for multiple benchmark results."""
45
+ if console is None:
46
+ console = Console()
47
+
48
+ table = Table(title=title)
49
+
50
+ table.add_column("Metric", style="cyan", no_wrap=True)
51
+
52
+ for comp in comparisons:
53
+ name = comp.get("name", "Benchmark")
54
+ table.add_column(name, style="green", justify="right")
55
+
56
+ if len(comparisons) == 2:
57
+ table.add_column("Difference", style="yellow", justify="right")
58
+
59
+ def get_metric_values(metric: str):
60
+ values = []
61
+ for comp in comparisons:
62
+ result = comp["result"]
63
+ if metric in result:
64
+ values.append(format_time(result[metric]))
65
+ else:
66
+ values.append("N/A")
67
+ return values
68
+
69
+ metrics = ["min", "max", "mean", "median", "stddev"]
70
+
71
+ for metric in metrics:
72
+ row = [metric.title()]
73
+ row.extend(get_metric_values(metric))
74
+
75
+ if len(comparisons) == 2:
76
+ result1 = comparisons[0]["result"]
77
+ result2 = comparisons[1]["result"]
78
+
79
+ if metric in result1 and metric in result2:
80
+ val1, val2 = result1[metric], result2[metric]
81
+ if val1 > 0:
82
+ diff_percent = ((val2 - val1) / val1) * 100
83
+ speedup = val1 / val2
84
+
85
+ if metric == "mean":
86
+ row.append(format_speedup(speedup))
87
+ else:
88
+ row.append(format_percentage(diff_percent))
89
+ else:
90
+ row.append("N/A")
91
+ else:
92
+ row.append("N/A")
93
+
94
+ table.add_row(*row)
95
+
96
+ config_row = ["Rounds"]
97
+ config_row.extend(
98
+ [str(comp["result"].get("rounds", "N/A")) for comp in comparisons]
99
+ )
100
+ if len(comparisons) == 2:
101
+ config_row.append("")
102
+ table.add_row(*config_row)
103
+
104
+ iter_row = ["Iterations"]
105
+ iter_row.extend(
106
+ [str(comp["result"].get("iterations", "N/A")) for comp in comparisons]
107
+ )
108
+ if len(comparisons) == 2:
109
+ iter_row.append("")
110
+ table.add_row(*iter_row)
111
+
112
+ console.print("\n")
113
+ console.print(table)
114
+
115
+ if len(comparisons) == 2:
116
+ result1 = comparisons[0]["result"]
117
+ result2 = comparisons[1]["result"]
118
+ name1 = comparisons[0].get("name", "Version 1")
119
+ name2 = comparisons[1].get("name", "Version 2")
120
+
121
+ if "mean" in result1 and "mean" in result2:
122
+ speedup = result1["mean"] / result2["mean"]
123
+ if speedup > 1.1:
124
+ console.print(f"๐Ÿ† {name2} is {format_speedup(speedup)} than {name1}")
125
+ elif speedup < 0.9:
126
+ console.print(
127
+ f"๐Ÿ† {name1} is {format_speedup(1 / speedup)} than {name2}"
128
+ )
129
+ else:
130
+ console.print(f"โš–๏ธ {name1} and {name2} have similar performance")
131
+
132
+ console.print("โœ… Comparison completed successfully!\n")
133
+
134
+
135
+ def validate_async_function(func) -> bool:
136
+ """Validate that a function is async."""
137
+ import asyncio
138
+
139
+ return asyncio.iscoroutinefunction(func)
140
+
141
+
142
+ def display_results_rich(benchmark_results, terminalreporter):
143
+ """Display benchmark results using rich."""
144
+ console = Console(file=terminalreporter._tw)
145
+
146
+ table = Table(title="Async Benchmark Results ๐Ÿ“Š")
147
+ table.add_column("Test Case ๐Ÿงช", style="cyan", no_wrap=True)
148
+ table.add_column("Min (ms)", style="magenta", justify="right")
149
+ table.add_column("Max (ms)", style="magenta", justify="right")
150
+ table.add_column("Mean (ms)", style="green", justify="right")
151
+ table.add_column("StdDev (ms)", style="yellow", justify="right")
152
+ table.add_column("Median (ms)", style="blue", justify="right")
153
+ table.add_column("Rounds ๐Ÿ”", justify="right")
154
+ table.add_column("Iter/Round ๐Ÿ”„", justify="right")
155
+
156
+ for result in benchmark_results:
157
+ status_emoji = "โœ…"
158
+ table.add_row(
159
+ f"{status_emoji} {result.name}",
160
+ format_time(result.min),
161
+ format_time(result.max),
162
+ format_time(result.mean),
163
+ format_time(result.stddev),
164
+ format_time(result.median),
165
+ str(result.rounds),
166
+ str(result.iterations),
167
+ )
168
+
169
+ console.print(table)
@@ -0,0 +1,91 @@
1
+ """Core pytest plugin for async benchmarking."""
2
+
3
+ import asyncio
4
+ from typing import Any, Callable, Optional
5
+
6
+ import pytest
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from .display import format_time
11
+ from .runner import AsyncBenchmarkRunner
12
+
13
+
14
+ class AsyncBenchmarkFixture:
15
+ """Fixture for benchmarking async functions."""
16
+
17
+ def __init__(self, request: pytest.FixtureRequest):
18
+ self.request = request
19
+ self.console = Console()
20
+ self.results: dict[str, Any] = {}
21
+
22
+ def __call__(
23
+ self,
24
+ func: Callable,
25
+ *args,
26
+ rounds: Optional[int] = None,
27
+ iterations: Optional[int] = None,
28
+ warmup_rounds: int = 1,
29
+ **kwargs,
30
+ ):
31
+ """Benchmark an async function."""
32
+ if not asyncio.iscoroutinefunction(func):
33
+ raise ValueError("Function must be async (coroutine function)")
34
+
35
+ runner = AsyncBenchmarkRunner(
36
+ rounds=rounds, iterations=iterations, warmup_rounds=warmup_rounds
37
+ )
38
+
39
+ try:
40
+ loop = asyncio.get_running_loop()
41
+ except RuntimeError:
42
+ loop = asyncio.new_event_loop()
43
+ asyncio.set_event_loop(loop)
44
+
45
+ result = loop.run_until_complete(runner.run(func, *args, **kwargs))
46
+
47
+ test_name = self.request.node.name
48
+ self.results[test_name] = result
49
+
50
+ self._display_results(test_name, result)
51
+
52
+ return result
53
+
54
+ def _display_results(self, test_name: str, result: dict[str, Any]):
55
+ """Display benchmark results using rich."""
56
+ table = Table(title=f"๐Ÿš€ Async Benchmark Results: {test_name}")
57
+
58
+ table.add_column("Metric", style="cyan")
59
+ table.add_column("Value", style="green")
60
+
61
+ table.add_row("Min", format_time(result["min"]))
62
+ table.add_row("Max", format_time(result["max"]))
63
+ table.add_row("Mean", format_time(result["mean"]))
64
+ table.add_row("Median", format_time(result["median"]))
65
+ table.add_row("Std Dev", format_time(result["stddev"]))
66
+ table.add_row("Rounds", str(result["rounds"]))
67
+ table.add_row("Iterations", str(result["iterations"]))
68
+
69
+ self.console.print("\n")
70
+ self.console.print(table)
71
+ self.console.print("โœ… Benchmark completed successfully!\n")
72
+
73
+
74
+ @pytest.fixture
75
+ def async_benchmark(request: pytest.FixtureRequest) -> AsyncBenchmarkFixture:
76
+ """Pytest fixture for async benchmarking."""
77
+ return AsyncBenchmarkFixture(request)
78
+
79
+
80
+ def pytest_configure(config):
81
+ """Configure pytest plugin."""
82
+ config.addinivalue_line(
83
+ "markers", "async_benchmark: mark test as an async benchmark"
84
+ )
85
+
86
+
87
+ def pytest_collection_modifyitems(config, items):
88
+ """Modify collected test items."""
89
+ for item in items:
90
+ if "async_benchmark" in item.fixturenames:
91
+ item.add_marker(pytest.mark.async_benchmark)
@@ -0,0 +1,56 @@
1
+ """Core benchmarking logic for async functions."""
2
+
3
+ import asyncio
4
+ import statistics
5
+ import time
6
+ from typing import Any, Callable, Optional
7
+
8
+
9
+ class AsyncBenchmarkRunner:
10
+ """Runner for async function benchmarks."""
11
+
12
+ def __init__(
13
+ self,
14
+ rounds: Optional[int] = None,
15
+ iterations: Optional[int] = None,
16
+ warmup_rounds: int = 1,
17
+ ):
18
+ self.rounds = rounds or 5
19
+ self.iterations = iterations or 1
20
+ self.warmup_rounds = warmup_rounds
21
+
22
+ async def run(self, func: Callable, *args, **kwargs) -> dict[str, Any]:
23
+ """Run benchmark for an async function."""
24
+ if not asyncio.iscoroutinefunction(func):
25
+ raise ValueError("Function must be async (coroutine function)")
26
+
27
+ for _ in range(self.warmup_rounds):
28
+ await func(*args, **kwargs)
29
+
30
+ times: list[float] = []
31
+
32
+ for _ in range(self.rounds):
33
+ round_times: list[float] = []
34
+
35
+ for _ in range(self.iterations):
36
+ start_time = time.perf_counter()
37
+ await func(*args, **kwargs)
38
+ end_time = time.perf_counter()
39
+ round_times.append(end_time - start_time)
40
+
41
+ times.append(statistics.mean(round_times))
42
+
43
+ return self._calculate_stats(times)
44
+
45
+ def _calculate_stats(self, times: list[float]) -> dict[str, Any]:
46
+ """Calculate statistics from timing data."""
47
+ return {
48
+ "min": min(times),
49
+ "max": max(times),
50
+ "mean": statistics.mean(times),
51
+ "median": statistics.median(times),
52
+ "stddev": statistics.stdev(times) if len(times) > 1 else 0.0,
53
+ "rounds": self.rounds,
54
+ "iterations": self.iterations,
55
+ "raw_times": times,
56
+ }
@@ -0,0 +1,36 @@
1
+ """Statistics calculation for async benchmarks."""
2
+
3
+ import statistics
4
+ from typing import Any
5
+
6
+
7
+ class StatsContainer:
8
+ """Container for benchmark statistics calculation."""
9
+
10
+ def __init__(self):
11
+ self.measurements: list[float] = []
12
+
13
+ def add_measurement(self, duration: float):
14
+ """Add a timing measurement."""
15
+ self.measurements.append(duration)
16
+
17
+ def calculate_stats(self) -> dict[str, Any]:
18
+ """Calculate comprehensive statistics."""
19
+ if not self.measurements:
20
+ return {}
21
+
22
+ return {
23
+ "min": min(self.measurements),
24
+ "max": max(self.measurements),
25
+ "mean": statistics.mean(self.measurements),
26
+ "median": statistics.median(self.measurements),
27
+ "stddev": statistics.stdev(self.measurements)
28
+ if len(self.measurements) > 1
29
+ else 0.0,
30
+ "count": len(self.measurements),
31
+ "raw_times": self.measurements.copy(),
32
+ }
33
+
34
+ def reset(self):
35
+ """Reset all measurements."""
36
+ self.measurements.clear()
@@ -0,0 +1,30 @@
1
+ """Utility functions for async benchmarking."""
2
+
3
+ import asyncio
4
+
5
+
6
+ def format_time(seconds: float) -> str:
7
+ """Format time duration in human-readable format."""
8
+ if seconds >= 1.0:
9
+ return f"{seconds:.3f}s"
10
+ elif seconds >= 0.001:
11
+ return f"{seconds * 1000:.3f}ms"
12
+ elif seconds >= 0.000001:
13
+ return f"{seconds * 1000000:.3f}ฮผs"
14
+ else:
15
+ return f"{seconds * 1000000000:.3f}ns"
16
+
17
+
18
+ def validate_async_function(func) -> bool:
19
+ """Validate that a function is async."""
20
+ return asyncio.iscoroutinefunction(func)
21
+
22
+
23
+ def get_event_loop():
24
+ """Get or create an event loop safely."""
25
+ try:
26
+ return asyncio.get_running_loop()
27
+ except RuntimeError:
28
+ loop = asyncio.new_event_loop()
29
+ asyncio.set_event_loop(loop)
30
+ return loop
@@ -0,0 +1,518 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-async-benchmark
3
+ Version: 0.1.1
4
+ Summary: pytest-async-benchmark: Modern pytest benchmarking for async code. ๐Ÿš€
5
+ Author-email: Mihai Farcas <contact@mihai.ltd>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Mihai Farcas <contact@mihai.ltd>
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.9
29
+ Requires-Dist: pytest>=8.3.5
30
+ Requires-Dist: rich>=14.0.0
31
+ Description-Content-Type: text/markdown
32
+
33
+ # pytest-async-benchmark ๐Ÿš€
34
+
35
+ Modern pytest benchmarking for async code with beautiful terminal output and advanced comparison tools.
36
+
37
+ ## โœจ Features
38
+
39
+ - ๐ŸŽฏ **Async-First**: Designed specifically for benchmarking `async def` functions
40
+ - ๐Ÿ”Œ **Pytest Integration**: Seamless integration as a pytest plugin
41
+ - ๐ŸŽจ **Rich Output**: Beautiful terminal reporting!
42
+ - ๐Ÿ“Š **Comprehensive Stats**: Min, max, mean, median, std dev, percentiles, and more
43
+ - โš–๏ธ **A vs B Comparisons**: Compare different implementations side-by-side
44
+ - ๐Ÿ“ˆ **Multi-Scenario Analysis**: Benchmark multiple scenarios with detailed comparison tables
45
+ - ๐ŸŽฏ **Performance Grading**: Automatic performance scoring and analysis
46
+ - โšก **Auto Calibration**: Intelligent round and iteration detection
47
+ - ๐Ÿ”„ **Quick Compare**: One-line comparison utilities
48
+ - ๐Ÿ† **Winner Detection**: Automatic identification of best-performing implementation
49
+ - ๐Ÿš€ **Easy to Use**: Simple fixture-based API
50
+
51
+ ## ๐Ÿ“ฆ Installation
52
+
53
+ ```bash
54
+ uv add pytest-async-benchmark
55
+ # or
56
+ pip install pytest-async-benchmark
57
+ ```
58
+
59
+ ## ๐Ÿš€ Quick Start
60
+
61
+ ```python
62
+ import asyncio
63
+ import pytest
64
+
65
+ async def slow_async_operation():
66
+ await asyncio.sleep(0.01) # 10ms
67
+ return "result"
68
+
69
+ def test_async_performance(async_benchmark):
70
+ result = async_benchmark(slow_async_operation, rounds=5)
71
+
72
+ # Your assertions here
73
+ assert result['mean'] < 0.02 # Should complete in under 20ms
74
+ ```
75
+
76
+ ## ๐ŸŽฏ Core Usage Examples
77
+
78
+ ### Basic Benchmarking
79
+
80
+ ```python
81
+ def test_my_async_function(async_benchmark):
82
+ async def my_function():
83
+ # Your async code here
84
+ await some_async_operation()
85
+ return result
86
+
87
+ # Benchmark with default settings (5 rounds, 1 iteration each)
88
+ stats = async_benchmark(my_function)
89
+
90
+ # Access comprehensive timing statistics
91
+ print(f"Mean execution time: {stats['mean']:.3f}s")
92
+ print(f"Standard deviation: {stats['stddev']:.3f}s")
93
+ print(f"95th percentile: {stats['p95']:.3f}s")
94
+ ```
95
+
96
+ ### Advanced Configuration
97
+
98
+ ```python
99
+ def test_with_custom_settings(async_benchmark):
100
+ result = async_benchmark(
101
+ my_async_function,
102
+ rounds=10, # Number of rounds to run
103
+ iterations=5, # Iterations per round
104
+ warmup_rounds=2 # Warmup rounds before measurement
105
+ )
106
+ ```
107
+
108
+ ### With Function Arguments
109
+
110
+ ```python
111
+ def test_with_args(async_benchmark):
112
+ async def process_data(data, multiplier=1):
113
+ # Process the data
114
+ await asyncio.sleep(0.01)
115
+ return len(data) * multiplier
116
+
117
+ result = async_benchmark(
118
+ process_data,
119
+ "test_data", # positional arg
120
+ multiplier=2, # keyword arg
121
+ rounds=3
122
+ )
123
+ ```
124
+
125
+ ## โš–๏ธ A vs B Comparison Features
126
+
127
+ ### Quick Comparison
128
+
129
+ ```python
130
+ from pytest_async_benchmark import quick_compare
131
+
132
+ async def algorithm_v1():
133
+ await asyncio.sleep(0.002) # 2ms
134
+ return "v1_result"
135
+
136
+ async def algorithm_v2():
137
+ await asyncio.sleep(0.0015) # 1.5ms - optimized
138
+ return "v2_result"
139
+
140
+ # Quick one-liner comparison
141
+ def test_algorithm_comparison():
142
+ winner, results = quick_compare(algorithm_v1, algorithm_v2, rounds=5)
143
+ assert winner == "algorithm_v2" # v2 should be faster
144
+ ```
145
+
146
+ ### Detailed A vs B Analysis
147
+
148
+ ```python
149
+ from pytest_async_benchmark import a_vs_b_comparison
150
+
151
+ def test_detailed_comparison():
152
+ # Compare with beautiful terminal output
153
+ a_vs_b_comparison(
154
+ "Original Algorithm", algorithm_v1,
155
+ "Optimized Algorithm", algorithm_v2,
156
+ rounds=8, iterations=20
157
+ )
158
+ ```
159
+
160
+ ### Multi-Scenario Benchmarking
161
+
162
+ ```python
163
+ from pytest_async_benchmark import BenchmarkComparator
164
+
165
+ def test_multi_scenario():
166
+ comparator = BenchmarkComparator()
167
+
168
+ # Add multiple scenarios
169
+ comparator.add_scenario(
170
+ "Database Query v1", db_query_v1,
171
+ rounds=5, iterations=10,
172
+ description="Original database implementation"
173
+ )
174
+
175
+ comparator.add_scenario(
176
+ "Database Query v2", db_query_v2,
177
+ rounds=5, iterations=10,
178
+ description="Optimized with connection pooling"
179
+ )
180
+
181
+ # Run comparison and get results
182
+ results = comparator.run_comparison()
183
+
184
+ # Beautiful comparison table automatically displayed
185
+ # Access programmatic results
186
+ fastest = results.get_fastest_scenario()
187
+ assert fastest.name == "Database Query v2"
188
+ ```
189
+
190
+ ## ๐Ÿ“Š Comprehensive Statistics
191
+
192
+ Each benchmark returns detailed statistics:
193
+
194
+ ```python
195
+ {
196
+ 'min': 0.001234, # Minimum execution time
197
+ 'max': 0.005678, # Maximum execution time
198
+ 'mean': 0.002456, # Mean execution time
199
+ 'median': 0.002123, # Median execution time
200
+ 'stddev': 0.000234, # Standard deviation
201
+ 'p50': 0.002123, # 50th percentile (median)
202
+ 'p90': 0.003456, # 90th percentile
203
+ 'p95': 0.004123, # 95th percentile
204
+ 'p99': 0.004789, # 99th percentile
205
+ 'rounds': 5, # Number of rounds executed
206
+ 'iterations': 1, # Number of iterations per round
207
+ 'raw_times': [...], # List of raw timing measurements
208
+ 'grade': 'A', # Performance grade (A-F)
209
+ 'grade_score': 87.5 # Numeric grade score (0-100)
210
+ }
211
+ ```
212
+
213
+ ## ๐ŸŽจ Beautiful Terminal Output
214
+
215
+ ### Basic Benchmark Output
216
+
217
+ ```
218
+ ๐Ÿš€ Async Benchmark Results: test_my_function
219
+ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“
220
+ โ”ƒ Metric โ”ƒ Value โ”ƒ
221
+ โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ
222
+ โ”‚ Min โ”‚ 10.234ms โ”‚
223
+ โ”‚ Max โ”‚ 15.678ms โ”‚
224
+ โ”‚ Mean โ”‚ 12.456ms โ”‚
225
+ โ”‚ Median โ”‚ 12.123ms โ”‚
226
+ โ”‚ Std Dev โ”‚ 1.234ms โ”‚
227
+ โ”‚ 95th %ile โ”‚ 14.567ms โ”‚
228
+ โ”‚ 99th %ile โ”‚ 15.234ms โ”‚
229
+ โ”‚ Grade โ”‚ A (87.5) โ”‚
230
+ โ”‚ Rounds โ”‚ 5 โ”‚
231
+ โ”‚ Iterations โ”‚ 1 โ”‚
232
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
233
+ โœ… Benchmark completed successfully!
234
+ ```
235
+
236
+ ### A vs B Comparison Output
237
+
238
+ ```
239
+ โš–๏ธ A vs B Comparison Results
240
+ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“
241
+ โ”ƒ Scenario โ”ƒ Algorithm A โ”ƒ Algorithm B โ”ƒ Winner โ”ƒ
242
+ โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ
243
+ โ”‚ Mean Time โ”‚ 2.456ms โ”‚ 1.789ms โ”‚ B ๐Ÿ† โ”‚
244
+ โ”‚ Median Time โ”‚ 2.234ms โ”‚ 1.678ms โ”‚ B ๐Ÿ† โ”‚
245
+ โ”‚ 95th Percentile โ”‚ 3.456ms โ”‚ 2.345ms โ”‚ B ๐Ÿ† โ”‚
246
+ โ”‚ Standard Deviation โ”‚ 0.567ms โ”‚ 0.234ms โ”‚ B ๐Ÿ† โ”‚
247
+ โ”‚ Performance Grade โ”‚ B (76.2) โ”‚ A (89.1) โ”‚ B ๐Ÿ† โ”‚
248
+ โ”‚ Improvement โ”‚ - โ”‚ 27.2% โ”‚ - โ”‚
249
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
250
+ ๐Ÿ† Winner: Algorithm B (27.2% faster)
251
+ ```
252
+
253
+ ## ๐Ÿ—๏ธ Project Structure
254
+
255
+ ```
256
+ pytest-async-benchmark/
257
+ โ”œโ”€โ”€ src/
258
+ โ”‚ โ””โ”€โ”€ pytest_async_benchmark/
259
+ โ”‚ โ”œโ”€โ”€ __init__.py # Main exports and API
260
+ โ”‚ โ”œโ”€โ”€ plugin.py # Pytest plugin and fixtures
261
+ โ”‚ โ”œโ”€โ”€ runner.py # Core benchmarking engine
262
+ โ”‚ โ”œโ”€โ”€ display.py # Rich terminal output formatting
263
+ โ”‚ โ”œโ”€โ”€ stats.py # Statistical calculations
264
+ โ”‚ โ”œโ”€โ”€ utils.py # Utility functions
265
+ โ”‚ โ”œโ”€โ”€ analytics.py # Performance analysis tools
266
+ โ”‚ โ””โ”€โ”€ comparison.py # A vs B comparison functionality
267
+ โ”œโ”€โ”€ examples/
268
+ โ”‚ โ”œโ”€โ”€ pytest_examples.py # Comprehensive pytest usage examples
269
+ โ”‚ โ”œโ”€โ”€ quart_api_comparison.py # Real-world API endpoint comparison
270
+ โ”‚ โ””โ”€โ”€ comparison_examples.py # Advanced comparison features demo
271
+ โ”œโ”€โ”€ tests/
272
+ โ”‚ โ”œโ”€โ”€ test_async_bench.py # Core functionality tests
273
+ โ”‚ โ”œโ”€โ”€ test_comparison.py # Comparison feature tests
274
+ โ”‚ โ”œโ”€โ”€ test_demo.py # Demo test cases
275
+ โ”‚ โ””โ”€โ”€ conftest.py # Test configuration
276
+ โ”œโ”€โ”€ pyproject.toml # Package configuration
277
+ โ””โ”€โ”€ README.md # This file
278
+ ```
279
+
280
+ ## ๐Ÿ“š Example Files Guide
281
+
282
+ ### ๐Ÿ”ง [`examples/pytest_examples.py`](examples/pytest_examples.py)
283
+ Comprehensive pytest usage examples including:
284
+ - Basic benchmarking with the `async_benchmark` fixture
285
+ - Advanced configuration options
286
+ - Performance assertions and testing patterns
287
+ - Using markers for benchmark organization
288
+
289
+ ### ๐ŸŒ [`examples/quart_api_comparison.py`](examples/quart_api_comparison.py)
290
+ Real-world API endpoint comparison demo featuring:
291
+ - Quart web framework setup
292
+ - API v1 vs v2 endpoint benchmarking
293
+ - Live server testing with actual HTTP requests
294
+ - Performance regression detection
295
+
296
+ ### โš–๏ธ [`examples/comparison_examples.py`](examples/comparison_examples.py)
297
+ Advanced comparison features showcase:
298
+ - Multi-scenario benchmark comparisons
299
+ - A vs B testing with detailed analysis
300
+ - Performance grading and scoring
301
+ - Statistical comparison utilities
302
+
303
+ ## ๐ŸŒ Real-World Examples
304
+
305
+ ### FastAPI Endpoint Benchmarking
306
+
307
+ ```python
308
+ from fastapi import FastAPI
309
+ from fastapi.testclient import TestClient
310
+ import pytest
311
+
312
+ app = FastAPI()
313
+
314
+ @app.get("/api/data")
315
+ async def get_data():
316
+ # Simulate database query
317
+ await asyncio.sleep(0.005)
318
+ return {"data": "example"}
319
+
320
+ def test_fastapi_endpoint_performance(async_benchmark):
321
+ async def make_request():
322
+ with TestClient(app) as client:
323
+ response = client.get("/api/data")
324
+ return response.json()
325
+
326
+ result = async_benchmark(make_request, rounds=10)
327
+ assert result['mean'] < 0.1 # Should respond within 100ms
328
+ assert result['grade'] in ['A', 'B'] # Should have good performance grade
329
+ ```
330
+
331
+ ### Quart API Endpoint Comparison
332
+
333
+ See the complete example in [`examples/quart_api_comparison.py`](examples/quart_api_comparison.py):
334
+
335
+ ```python
336
+ from pytest_async_benchmark import a_vs_b_comparison
337
+ import asyncio
338
+ import aiohttp
339
+
340
+ async def test_api_v1():
341
+ async with aiohttp.ClientSession() as session:
342
+ async with session.get('http://localhost:5000/api/v1/data') as resp:
343
+ return await resp.json()
344
+
345
+ async def test_api_v2():
346
+ async with aiohttp.ClientSession() as session:
347
+ async with session.get('http://localhost:5000/api/v2/data') as resp:
348
+ return await resp.json()
349
+
350
+ # Compare API versions
351
+ a_vs_b_comparison(
352
+ "API v1", test_api_v1,
353
+ "API v2 (Optimized)", test_api_v2,
354
+ rounds=10, iterations=5
355
+ )
356
+ ```
357
+
358
+ ### Database Query Benchmarking
359
+
360
+ ```python
361
+ async def test_database_query_performance(async_benchmark):
362
+ async def fetch_user_data(user_id):
363
+ async with database.connection() as conn:
364
+ return await conn.fetch_one(
365
+ "SELECT * FROM users WHERE id = ?", user_id
366
+ )
367
+
368
+ result = async_benchmark(fetch_user_data, 123, rounds=5)
369
+ assert result['mean'] < 0.05 # Should complete within 50ms
370
+ assert result['p95'] < 0.1 # 95% of queries under 100ms
371
+ ```
372
+
373
+ ## ๐ŸŽฏ Using Markers
374
+
375
+ ```python
376
+ @pytest.mark.async_benchmark
377
+ def test_performance(async_benchmark):
378
+ # Your benchmark test
379
+ result = async_benchmark(my_async_function)
380
+ assert result is not None
381
+ ```
382
+
383
+ ## ๐Ÿ“‹ API Reference
384
+
385
+ ### `async_benchmark(func, *args, rounds=None, iterations=None, warmup_rounds=1, **kwargs)`
386
+
387
+ **Parameters:**
388
+ - `func`: The async function to benchmark
389
+ - `*args`: Positional arguments to pass to the function
390
+ - `rounds`: Number of measurement rounds (default: 5)
391
+ - `iterations`: Number of iterations per round (default: 1)
392
+ - `warmup_rounds`: Number of warmup rounds before measurement (default: 1)
393
+ - `**kwargs`: Keyword arguments to pass to the function
394
+
395
+ **Returns:**
396
+ A dictionary with comprehensive statistics including min, max, mean, median, stddev, percentiles, performance grade, and raw measurements.
397
+
398
+ ### Comparison Functions
399
+
400
+ - `quick_compare(func_a, func_b, **kwargs)`: Quick comparison returning winner and results
401
+ - `a_vs_b_comparison(name_a, func_a, name_b, func_b, **kwargs)`: Detailed comparison with terminal output
402
+ - `BenchmarkComparator`: Class for multi-scenario benchmarking and analysis
403
+
404
+ ## ๐Ÿ“‹ Requirements
405
+
406
+ - Python โ‰ฅ 3.9
407
+ - pytest โ‰ฅ 8.3.5
408
+
409
+ Note: Rich (for beautiful terminal output) is automatically installed as a dependency.
410
+
411
+ ## ๐Ÿš€ Development
412
+
413
+ ```bash
414
+ # Clone the repository
415
+ git clone https://github.com/yourusername/pytest-async-benchmark.git
416
+ cd pytest-async-benchmark
417
+
418
+ # Install dependencies
419
+ uv sync
420
+
421
+ # Run tests
422
+ uv run pytest tests/ -v
423
+
424
+ # Run examples
425
+ uv run pytest examples/pytest_examples.py -v
426
+
427
+ # Test real-world Quart API comparison
428
+ uv run python examples/quart_api_comparison.py
429
+
430
+ # See advanced comparison features
431
+ uv run python examples/comparison_examples.py
432
+ ```
433
+
434
+ ### ๐Ÿ› ๏ธ Code Quality and Formatting
435
+
436
+ This project uses [Ruff](https://docs.astral.sh/ruff/) for both linting and formatting:
437
+
438
+ ```bash
439
+ # Check code for linting issues
440
+ uv run ruff check .
441
+
442
+ # Fix auto-fixable linting issues
443
+ uv run ruff check . --fix
444
+
445
+ # Check code formatting
446
+ uv run ruff format --check .
447
+
448
+ # Format code automatically
449
+ uv run ruff format .
450
+
451
+ # Run both linting and formatting in one go
452
+ uv run ruff check . --fix && uv run ruff format .
453
+
454
+ # Run all quality checks at once (linting, formatting, and tests)
455
+ uv run python scripts/quality-check.py
456
+ ```
457
+
458
+ ### ๐Ÿ“‹ Release Readiness Check
459
+
460
+ Before creating a release, verify everything is ready:
461
+
462
+ ```bash
463
+ # Run comprehensive release check
464
+ uv run python scripts/release-check.py
465
+
466
+ # This checks:
467
+ # โœ… Git repository status
468
+ # โœ… Version consistency
469
+ # โœ… Code formatting and linting
470
+ # โœ… Test suite passes
471
+ # โœ… Package builds successfully
472
+ # โœ… All required files exist
473
+ ```
474
+
475
+ ### ๐Ÿš€ Quick Quality Check
476
+
477
+ Run all quality checks at once:
478
+
479
+ ```bash
480
+ # Run linting, formatting, tests, and release checks
481
+ python scripts/quality-check.py
482
+
483
+ # This will:
484
+ # ๐Ÿ”ง Fix linting issues automatically
485
+ # ๐ŸŽจ Format code with Ruff
486
+ # ๐Ÿงช Run the full test suite
487
+ # ๐Ÿ“‹ Check release readiness
488
+ ```
489
+
490
+ ## ๐Ÿš€ Automated Releases
491
+
492
+ This project uses GitHub Actions for automated testing and publishing to PyPI:
493
+
494
+ - **Continuous Integration**: Tests run on every push for Python 3.9-3.13
495
+ - **Test Publishing**: Automatic uploads to TestPyPI for testing releases
496
+ - **Production Releases**: Secure publishing to PyPI using trusted publishing
497
+ - **Release Validation**: Comprehensive checks ensure package quality
498
+
499
+ ### Creating a Release
500
+
501
+ 1. Update version in `pyproject.toml` and `src/pytest_async_benchmark/__init__.py`
502
+ 2. Run `uv run python scripts/release-check.py` to verify readiness
503
+ 3. Create a git tag: `git tag v1.0.0 && git push origin v1.0.0`
504
+ 4. Create a GitHub release to trigger automated PyPI publishing
505
+
506
+ See [RELEASE_GUIDE.md](RELEASE_GUIDE.md) for detailed release instructions.
507
+
508
+ ## ๐Ÿค Contributing
509
+
510
+ Contributions are welcome! Please feel free to submit a Pull Request.
511
+
512
+ ## ๐Ÿ“„ License
513
+
514
+ MIT License - see [LICENSE](LICENSE) file for details.
515
+
516
+ ---
517
+
518
+ *Built with โค๏ธ for the async Python community*
@@ -0,0 +1,13 @@
1
+ pytest_async_benchmark/__init__.py,sha256=i_vjO6kOxhDSBXhBuVo9mgvItucMaTAne_jaVorarNg,825
2
+ pytest_async_benchmark/analytics.py,sha256=7lR-GF0rqS8X1pVtFucmyO-GXvzYXoX0I9bOh6uzUhQ,1710
3
+ pytest_async_benchmark/comparison.py,sha256=-TXaUSpFNKzroPRRILw_7aAOte5uARSiaDeifShUgUQ,8025
4
+ pytest_async_benchmark/display.py,sha256=WRoiSZK03XNAtfeMzBniJeHra2YzPB4_DbWsMp30BVI,5357
5
+ pytest_async_benchmark/plugin.py,sha256=P8TcblejlVejdDIowy4Zeame-C4pa23IEBS_rFqqIgs,2857
6
+ pytest_async_benchmark/runner.py,sha256=HFRZ8kc9fDNGy9IorZawAKN9KnQjQv_YqdvV-qbAt_Y,1775
7
+ pytest_async_benchmark/stats.py,sha256=I0hpi-TqcstOdXxLW03p1ZA7TX1qz9JnJNu_vSxWqWA,1074
8
+ pytest_async_benchmark/utils.py,sha256=-oiEiuOXyiU614wkBgS67em1KVQCnaJW4z_x3lNnSKs,797
9
+ pytest_async_benchmark-0.1.1.dist-info/METADATA,sha256=XgpF-xFS1Ndw5_juXkzRAE5s6K3YEpzLFf26q0yTV8E,16976
10
+ pytest_async_benchmark-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ pytest_async_benchmark-0.1.1.dist-info/entry_points.txt,sha256=5PoCegPmQcqj3Y5iGuiW9pGvYxnxirJ-P93NekT7aYg,55
12
+ pytest_async_benchmark-0.1.1.dist-info/licenses/LICENSE,sha256=ZzNL_t8ZeKS3i3duN9drmwyTr5DiTTCAtpgTB8JXH1k,1088
13
+ pytest_async_benchmark-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ async_bench = pytest_async_benchmark.plugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mihai Farcas <contact@mihai.ltd>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.