provide-testkit 0.0.0.dev0__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.
Files changed (66) hide show
  1. provide/__init__.py +3 -0
  2. provide/testkit/__init__.py +248 -0
  3. provide/testkit/archive/__init__.py +24 -0
  4. provide/testkit/archive/fixtures.py +217 -0
  5. provide/testkit/cli.py +229 -0
  6. provide/testkit/common/__init__.py +32 -0
  7. provide/testkit/common/fixtures.py +234 -0
  8. provide/testkit/crypto.py +163 -0
  9. provide/testkit/environment.py +79 -0
  10. provide/testkit/file/__init__.py +40 -0
  11. provide/testkit/file/content_fixtures.py +275 -0
  12. provide/testkit/file/directory_fixtures.py +105 -0
  13. provide/testkit/file/fixtures.py +49 -0
  14. provide/testkit/file/special_fixtures.py +141 -0
  15. provide/testkit/fixtures.py +52 -0
  16. provide/testkit/harness.py +122 -0
  17. provide/testkit/hub.py +22 -0
  18. provide/testkit/logger/__init__.py +39 -0
  19. provide/testkit/logger/hooks.py +100 -0
  20. provide/testkit/logger/reset.py +230 -0
  21. provide/testkit/main.py +22 -0
  22. provide/testkit/mocking/__init__.py +46 -0
  23. provide/testkit/mocking/fixtures.py +340 -0
  24. provide/testkit/process/__init__.py +48 -0
  25. provide/testkit/process/async_fixtures.py +410 -0
  26. provide/testkit/process/fixtures.py +54 -0
  27. provide/testkit/process/subprocess_fixtures.py +208 -0
  28. provide/testkit/quality/__init__.py +101 -0
  29. provide/testkit/quality/artifacts.py +360 -0
  30. provide/testkit/quality/base.py +158 -0
  31. provide/testkit/quality/cli.py +394 -0
  32. provide/testkit/quality/complexity/__init__.py +30 -0
  33. provide/testkit/quality/complexity/analyzer.py +392 -0
  34. provide/testkit/quality/complexity/fixture.py +196 -0
  35. provide/testkit/quality/coverage/__init__.py +36 -0
  36. provide/testkit/quality/coverage/fixture.py +236 -0
  37. provide/testkit/quality/coverage/reporter.py +150 -0
  38. provide/testkit/quality/coverage/tracker.py +313 -0
  39. provide/testkit/quality/decorators.py +380 -0
  40. provide/testkit/quality/documentation/__init__.py +29 -0
  41. provide/testkit/quality/documentation/checker.py +361 -0
  42. provide/testkit/quality/documentation/fixture.py +187 -0
  43. provide/testkit/quality/profiling/__init__.py +30 -0
  44. provide/testkit/quality/profiling/fixture.py +332 -0
  45. provide/testkit/quality/profiling/profiler.py +428 -0
  46. provide/testkit/quality/report.py +266 -0
  47. provide/testkit/quality/runner.py +319 -0
  48. provide/testkit/quality/security/__init__.py +29 -0
  49. provide/testkit/quality/security/fixture.py +196 -0
  50. provide/testkit/quality/security/scanner.py +338 -0
  51. provide/testkit/streams.py +54 -0
  52. provide/testkit/threading/__init__.py +38 -0
  53. provide/testkit/threading/basic_fixtures.py +103 -0
  54. provide/testkit/threading/data_fixtures.py +101 -0
  55. provide/testkit/threading/execution_fixtures.py +268 -0
  56. provide/testkit/threading/fixtures.py +50 -0
  57. provide/testkit/threading/sync_fixtures.py +98 -0
  58. provide/testkit/time/__init__.py +32 -0
  59. provide/testkit/time/fixtures.py +416 -0
  60. provide/testkit/transport/__init__.py +30 -0
  61. provide/testkit/transport/fixtures.py +278 -0
  62. provide_testkit-0.0.0.dev0.dist-info/METADATA +145 -0
  63. provide_testkit-0.0.0.dev0.dist-info/RECORD +66 -0
  64. provide_testkit-0.0.0.dev0.dist-info/WHEEL +5 -0
  65. provide_testkit-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  66. provide_testkit-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,361 @@
1
+ """Documentation coverage checker implementation using interrogate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ import time
8
+ from typing import Any
9
+
10
+ from provide.foundation.file import atomic_write_text, ensure_dir
11
+
12
+ try:
13
+ import interrogate # type: ignore[import-untyped]
14
+ from interrogate import coverage
15
+ from interrogate.config import InterrogateConfig # type: ignore[import-untyped]
16
+
17
+ INTERROGATE_AVAILABLE = True
18
+ except ImportError:
19
+ INTERROGATE_AVAILABLE = False
20
+ interrogate = None
21
+ coverage = None
22
+ InterrogateConfig = None
23
+
24
+ from ..base import QualityResult, QualityToolError
25
+
26
+
27
+ class DocumentationChecker:
28
+ """Documentation coverage checker using interrogate.
29
+
30
+ Provides high-level interface for documentation analysis with automatic
31
+ artifact management and integration with the quality framework.
32
+ """
33
+
34
+ def __init__(self, config: dict[str, Any] | None = None):
35
+ """Initialize documentation checker.
36
+
37
+ Args:
38
+ config: Documentation checker configuration options
39
+ """
40
+ if not INTERROGATE_AVAILABLE:
41
+ raise QualityToolError(
42
+ "Interrogate not available. Install with: pip install interrogate", tool="documentation"
43
+ )
44
+
45
+ self.config = config or {}
46
+ self.artifact_dir: Path | None = None
47
+
48
+ def analyze(self, path: Path, **kwargs: Any) -> QualityResult:
49
+ """Run documentation analysis on the given path.
50
+
51
+ Args:
52
+ path: Path to analyze
53
+ **kwargs: Additional options including artifact_dir
54
+
55
+ Returns:
56
+ QualityResult with documentation analysis data
57
+ """
58
+ self.artifact_dir = kwargs.get("artifact_dir", Path(".documentation"))
59
+ start_time = time.time()
60
+
61
+ try:
62
+ # Run interrogate documentation analysis
63
+ result = self._run_interrogate_analysis(path)
64
+ result.execution_time = time.time() - start_time
65
+
66
+ # Generate artifacts
67
+ self._generate_artifacts(result)
68
+
69
+ return result
70
+
71
+ except Exception as e:
72
+ return QualityResult(
73
+ tool="documentation",
74
+ passed=False,
75
+ details={"error": str(e), "error_type": type(e).__name__},
76
+ execution_time=time.time() - start_time,
77
+ )
78
+
79
+ def _run_interrogate_analysis(self, path: Path) -> QualityResult:
80
+ """Run interrogate documentation analysis."""
81
+ if not INTERROGATE_AVAILABLE:
82
+ raise QualityToolError("Interrogate not available", tool="documentation")
83
+
84
+ try:
85
+ # Create interrogate configuration
86
+ config_args = self._build_interrogate_config()
87
+
88
+ # Create config object - filter out unsupported parameters
89
+ supported_args = {}
90
+ for key, value in config_args.items():
91
+ # Only include basic supported parameters for InterrogateConfig
92
+ if key in [
93
+ "ignore_init_method",
94
+ "ignore_magic",
95
+ "ignore_private",
96
+ "verbose",
97
+ "quiet",
98
+ "paths",
99
+ ]:
100
+ supported_args[key] = value
101
+
102
+ supported_args["paths"] = [str(path)]
103
+ config = InterrogateConfig(**supported_args)
104
+
105
+ # Run interrogate analysis
106
+ cov = coverage.InterrogateCoverage(config=config)
107
+ results = cov.get_coverage()
108
+
109
+ # Process results
110
+ return self._process_interrogate_results(results, config)
111
+
112
+ except Exception as e:
113
+ raise QualityToolError(f"Interrogate analysis failed: {e}", tool="documentation")
114
+
115
+ def _build_interrogate_config(self) -> dict[str, Any]:
116
+ """Build interrogate configuration from config."""
117
+ config = {}
118
+
119
+ # Set ignore patterns
120
+ ignore_patterns = self.config.get(
121
+ "ignore", ["__pycache__", "*.pyc", "test_*", "tests/*", "*/.venv/*", "*/venv/*"]
122
+ )
123
+ if ignore_patterns:
124
+ # Convert list to regex pattern for interrogate
125
+ pattern_string = "|".join(str(pattern) for pattern in ignore_patterns)
126
+ config["ignore_regex"] = pattern_string
127
+
128
+ # Set what to check - these are the keys that tests expect
129
+ config["ignore_init_method"] = self.config.get("ignore_init_method", True)
130
+ config["ignore_magic"] = self.config.get("ignore_magic", True)
131
+ config["ignore_private"] = self.config.get("ignore_private", False)
132
+ config["ignore_setters"] = self.config.get("ignore_setters", True)
133
+
134
+ # Verbosity and output
135
+ config["verbose"] = self.config.get("verbose", 0)
136
+ if self.config.get("quiet", False):
137
+ config["quiet"] = True
138
+
139
+ return config
140
+
141
+ def _process_interrogate_results(self, results: Any, config: Any) -> QualityResult:
142
+ """Process interrogate results into QualityResult."""
143
+ # Extract coverage metrics
144
+ total_coverage = results.perc_covered
145
+ missing_count = results.missing_count
146
+ covered_count = results.covered_count
147
+ total_count = missing_count + covered_count
148
+
149
+ # Calculate grade and score
150
+ grade, score = self._calculate_grade_and_score(total_coverage)
151
+
152
+ # Check if passed based on configuration
153
+ passed = self._check_documentation_requirements(total_coverage, grade, score)
154
+
155
+ # Create detailed results
156
+ details = self._build_documentation_details(
157
+ total_coverage, covered_count, missing_count, total_count, grade
158
+ )
159
+
160
+ # Add file-level details if available
161
+ self._add_file_coverage_details(results, details)
162
+
163
+ return QualityResult(tool="documentation", passed=passed, score=score, details=details)
164
+
165
+ def _calculate_grade_and_score(self, coverage: float) -> tuple[str, float]:
166
+ """Calculate grade and score based on coverage percentage."""
167
+ grade_thresholds = [
168
+ (95, "A", 100.0),
169
+ (90, "A-", 95.0),
170
+ (85, "B+", 90.0),
171
+ (80, "B", 85.0),
172
+ (75, "B-", 80.0),
173
+ (70, "C+", 75.0),
174
+ (65, "C", 70.0),
175
+ (60, "C-", 65.0),
176
+ (50, "D", 55.0),
177
+ ]
178
+
179
+ for threshold, grade, score in grade_thresholds:
180
+ if coverage >= threshold:
181
+ return grade, score
182
+
183
+ return "F", 40.0
184
+
185
+ def _check_documentation_requirements(self, coverage: float, grade: str, score: float) -> bool:
186
+ """Check if documentation meets the configured requirements."""
187
+ min_coverage = self.config.get("min_coverage", 80.0)
188
+ min_grade = self.config.get("min_grade", "C")
189
+ required_score = self.config.get("min_score", 70.0)
190
+
191
+ grade_values = {"A": 9, "A-": 8, "B+": 7, "B": 6, "B-": 5, "C+": 4, "C": 3, "C-": 2, "D": 1, "F": 0}
192
+
193
+ return (
194
+ coverage >= min_coverage
195
+ and grade_values.get(grade, 0) >= grade_values.get(min_grade, 0)
196
+ and score >= required_score
197
+ )
198
+
199
+ def _build_documentation_details(
200
+ self, coverage: float, covered: int, missing: int, total: int, grade: str
201
+ ) -> dict[str, Any]:
202
+ """Build the details dictionary for documentation results."""
203
+ min_coverage = self.config.get("min_coverage", 80.0)
204
+ min_grade = self.config.get("min_grade", "C")
205
+ required_score = self.config.get("min_score", 70.0)
206
+
207
+ return {
208
+ "total_coverage": round(coverage, 2),
209
+ "covered_count": covered,
210
+ "missing_count": missing,
211
+ "total_count": total,
212
+ "grade": grade,
213
+ "thresholds": {"min_coverage": min_coverage, "min_grade": min_grade, "min_score": required_score},
214
+ }
215
+
216
+ def _add_file_coverage_details(self, results: Any, details: dict[str, Any]) -> None:
217
+ """Add file-level coverage details if available."""
218
+ if not (hasattr(results, "detailed_coverage") and results.detailed_coverage):
219
+ return
220
+
221
+ file_details = []
222
+ try:
223
+ for file_info in results.detailed_coverage:
224
+ file_details.append(
225
+ {
226
+ "file": str(file_info.filename),
227
+ "coverage": file_info.perc_covered,
228
+ "covered": file_info.covered_count,
229
+ "missing": file_info.missing_count,
230
+ }
231
+ )
232
+ details["file_coverage"] = file_details
233
+ except (TypeError, AttributeError):
234
+ # Skip file details if not properly formed
235
+ pass
236
+
237
+ def _generate_artifacts(self, result: QualityResult) -> None:
238
+ """Generate documentation analysis artifacts.
239
+
240
+ Args:
241
+ result: Result to add artifacts to
242
+ """
243
+ if not self.artifact_dir:
244
+ return
245
+
246
+ ensure_dir(self.artifact_dir)
247
+
248
+ try:
249
+ # Generate JSON report
250
+ json_file = self.artifact_dir / "documentation.json"
251
+ json_data = {
252
+ "tool": result.tool,
253
+ "passed": result.passed,
254
+ "score": result.score,
255
+ "details": result.details,
256
+ "execution_time": result.execution_time,
257
+ }
258
+ atomic_write_text(json_file, json.dumps(json_data, indent=2))
259
+ result.artifacts.append(json_file)
260
+
261
+ # Generate text summary
262
+ summary_file = self.artifact_dir / "documentation_summary.txt"
263
+ summary_report = self._generate_text_report(result)
264
+ atomic_write_text(summary_file, summary_report)
265
+ result.artifacts.append(summary_file)
266
+
267
+ # Generate detailed coverage report if available
268
+ if result.details.get("file_coverage"):
269
+ detail_file = self.artifact_dir / "documentation_details.txt"
270
+ detail_report = self._generate_detail_report(result)
271
+ atomic_write_text(detail_file, detail_report)
272
+ result.artifacts.append(detail_file)
273
+
274
+ except Exception as e:
275
+ # Add error to result details but don't fail
276
+ result.details["artifact_error"] = str(e)
277
+
278
+ def _generate_text_report(self, result: QualityResult) -> str:
279
+ """Generate text summary report."""
280
+ lines = [
281
+ f"Documentation Coverage Report - {result.tool}",
282
+ "=" * 50,
283
+ f"Status: {'✅ PASSED' if result.passed else '❌ FAILED'}",
284
+ f"Grade: {result.details.get('grade', 'N/A')}",
285
+ f"Coverage: {result.details.get('total_coverage', 0)}%",
286
+ f"Score: {result.score}%",
287
+ ]
288
+
289
+ details = result.details
290
+ if "covered_count" in details:
291
+ covered = details["covered_count"]
292
+ missing = details["missing_count"]
293
+ total = details.get("total_count", covered + missing)
294
+ lines.extend(
295
+ [
296
+ "",
297
+ f"Documented Items: {covered}",
298
+ f"Missing Documentation: {missing}",
299
+ f"Total Items: {total}",
300
+ ]
301
+ )
302
+
303
+ thresholds = details.get("thresholds", {})
304
+ if thresholds:
305
+ lines.extend(
306
+ [
307
+ "",
308
+ "Thresholds:",
309
+ f" Minimum Coverage: {thresholds.get('min_coverage', 0)}%",
310
+ f" Minimum Grade: {thresholds.get('min_grade', 'N/A')}",
311
+ f" Minimum Score: {thresholds.get('min_score', 0)}%",
312
+ ]
313
+ )
314
+
315
+ if result.execution_time:
316
+ lines.append(f"\nExecution Time: {result.execution_time:.2f}s")
317
+
318
+ return "\n".join(lines)
319
+
320
+ def _generate_detail_report(self, result: QualityResult) -> str:
321
+ """Generate detailed file coverage report."""
322
+ lines = ["Documentation Coverage by File", "=" * 50, ""]
323
+
324
+ file_coverage = result.details.get("file_coverage", [])
325
+
326
+ # Sort by coverage (lowest first to highlight problem files)
327
+ sorted_files = sorted(file_coverage, key=lambda x: x["coverage"])
328
+
329
+ for file_info in sorted_files:
330
+ coverage = file_info["coverage"]
331
+ status = "✅" if coverage >= 80 else "⚠️" if coverage >= 60 else "❌"
332
+ lines.append(
333
+ f"{status} {file_info['file']}: {coverage:.1f}% ({file_info['covered']}/{file_info['covered'] + file_info['missing']})"
334
+ )
335
+
336
+ return "\n".join(lines)
337
+
338
+ def report(self, result: QualityResult, format: str = "terminal") -> str:
339
+ """Generate report from QualityResult (implements QualityTool protocol).
340
+
341
+ Args:
342
+ result: Documentation result
343
+ format: Report format
344
+
345
+ Returns:
346
+ Formatted report
347
+ """
348
+ if format == "terminal":
349
+ return self._generate_text_report(result)
350
+ elif format == "json":
351
+ return json.dumps(
352
+ {
353
+ "tool": result.tool,
354
+ "passed": result.passed,
355
+ "score": result.score,
356
+ "details": result.details,
357
+ },
358
+ indent=2,
359
+ )
360
+ else:
361
+ return str(result.details)
@@ -0,0 +1,187 @@
1
+ """Documentation coverage fixture for pytest integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import pytest
9
+
10
+ from ..base import BaseQualityFixture
11
+
12
+ try:
13
+ from .checker import INTERROGATE_AVAILABLE, DocumentationChecker
14
+ except ImportError:
15
+ DocumentationChecker = None
16
+ INTERROGATE_AVAILABLE = False
17
+
18
+
19
+ class DocumentationFixture(BaseQualityFixture):
20
+ """Pytest fixture for documentation coverage analysis.
21
+
22
+ Provides easy access to documentation coverage checking with automatic
23
+ setup and teardown. Integrates with the quality framework fixtures.
24
+ """
25
+
26
+ def __init__(self, config: dict[str, Any] | None = None, artifact_dir: Path | None = None):
27
+ """Initialize documentation fixture.
28
+
29
+ Args:
30
+ config: Documentation checker configuration
31
+ artifact_dir: Directory for artifacts
32
+ """
33
+ super().__init__(config or {}, artifact_dir)
34
+ self.analyzer: DocumentationChecker | None = None
35
+
36
+ def setup(self) -> None:
37
+ """Set up documentation analyzer."""
38
+ if not INTERROGATE_AVAILABLE:
39
+ pytest.skip("interrogate not available")
40
+
41
+ self.analyzer = DocumentationChecker(self.config)
42
+ self._setup_complete = True
43
+
44
+ def teardown(self) -> None:
45
+ """Clean up documentation analyzer."""
46
+ self.analyzer = None
47
+ self._setup_complete = False
48
+
49
+ def analyze(self, path: Path) -> dict[str, Any]:
50
+ """Run documentation coverage analysis.
51
+
52
+ Args:
53
+ path: Path to analyze
54
+
55
+ Returns:
56
+ Analysis results as dict
57
+ """
58
+ if not self.analyzer:
59
+ return {"error": "Analyzer not available"}
60
+
61
+ result = self.analyzer.analyze(path, artifact_dir=self.artifact_dir)
62
+ self.add_result(result)
63
+
64
+ return {
65
+ "passed": result.passed,
66
+ "score": result.score,
67
+ "grade": result.details.get("grade"),
68
+ "total_coverage": result.details.get("total_coverage"),
69
+ "covered_count": result.details.get("covered_count"),
70
+ "missing_count": result.details.get("missing_count"),
71
+ "total_count": result.details.get("total_count"),
72
+ "file_coverage": result.details.get("file_coverage", []),
73
+ "thresholds": result.details.get("thresholds", {}),
74
+ "execution_time": result.execution_time,
75
+ }
76
+
77
+ def check(
78
+ self,
79
+ path: Path,
80
+ min_coverage: float | None = None,
81
+ min_grade: str | None = None,
82
+ min_score: float | None = None,
83
+ ) -> dict[str, Any]:
84
+ """Check documentation coverage with optional thresholds.
85
+
86
+ Args:
87
+ path: Path to check
88
+ min_coverage: Minimum coverage percentage required
89
+ min_grade: Minimum grade required (A, B, C, D, F)
90
+ min_score: Minimum score required
91
+
92
+ Returns:
93
+ Check results including pass/fail status
94
+ """
95
+ if not self._setup_complete:
96
+ self.setup()
97
+
98
+ # Update config with provided thresholds
99
+ if min_coverage is not None:
100
+ self.config["min_coverage"] = min_coverage
101
+ if min_grade is not None:
102
+ self.config["min_grade"] = min_grade
103
+ if min_score is not None:
104
+ self.config["min_score"] = min_score
105
+
106
+ # Recreate analyzer with updated config
107
+ if self.analyzer and any(x is not None for x in [min_coverage, min_grade, min_score]):
108
+ self.analyzer = DocumentationChecker(self.config)
109
+
110
+ return self.analyze(path)
111
+
112
+ def generate_report(self, format: str = "terminal") -> str:
113
+ """Generate documentation report.
114
+
115
+ Args:
116
+ format: Report format (terminal, json)
117
+
118
+ Returns:
119
+ Formatted report
120
+ """
121
+ if not self.analyzer:
122
+ return "No documentation analyzer available"
123
+
124
+ if not self.results:
125
+ return "No documentation results available"
126
+
127
+ # Use the most recent result
128
+ latest_result = self.results[-1]
129
+ return self.analyzer.report(latest_result, format)
130
+
131
+
132
+ @pytest.fixture
133
+ def documentation_checker() -> DocumentationFixture:
134
+ """Provide documentation coverage checker fixture.
135
+
136
+ Returns:
137
+ DocumentationFixture instance
138
+ """
139
+ fixture = DocumentationFixture()
140
+ fixture.setup()
141
+ yield fixture
142
+ fixture.teardown()
143
+
144
+
145
+ @pytest.fixture
146
+ def documentation_config() -> dict[str, Any]:
147
+ """Provide default documentation configuration.
148
+
149
+ Returns:
150
+ Default configuration for documentation checking
151
+ """
152
+ return {
153
+ "min_coverage": 80.0,
154
+ "min_grade": "C",
155
+ "min_score": 70.0,
156
+ "ignore_init_method": True,
157
+ "ignore_magic": True,
158
+ "ignore_setters": True,
159
+ "ignore": ["__pycache__", "*.pyc", "test_*", "tests/*", "*/.venv/*", "*/venv/*"],
160
+ }
161
+
162
+
163
+ @pytest.fixture
164
+ def documentation_checker_strict(documentation_config: dict[str, Any]) -> DocumentationFixture:
165
+ """Provide strict documentation checker fixture.
166
+
167
+ Args:
168
+ documentation_config: Base configuration
169
+
170
+ Returns:
171
+ DocumentationFixture with strict requirements
172
+ """
173
+ config = documentation_config.copy()
174
+ config.update(
175
+ {
176
+ "min_coverage": 95.0,
177
+ "min_grade": "A",
178
+ "min_score": 95.0,
179
+ "ignore_init_method": False,
180
+ "ignore_magic": False,
181
+ }
182
+ )
183
+
184
+ fixture = DocumentationFixture(config)
185
+ fixture.setup()
186
+ yield fixture
187
+ fixture.teardown()
@@ -0,0 +1,30 @@
1
+ """Performance profiling and analysis for provide-testkit.
2
+
3
+ Provides performance profiling analysis using memray, cProfile, and other tools.
4
+ Integrates with the quality framework for comprehensive performance analysis.
5
+
6
+ Features:
7
+ - Memory profiling with memray
8
+ - CPU profiling with cProfile
9
+ - Performance regression detection
10
+ - Integration with quality gates
11
+ - Configurable profiling options
12
+
13
+ Usage:
14
+ # Basic memory profiling
15
+ def test_with_profiling(profiling_fixture):
16
+ result = profiling_fixture.profile_memory(function, *args)
17
+ assert result.passed
18
+
19
+ # CPU profiling with quality gates
20
+ runner = QualityRunner()
21
+ results = runner.run_with_gates(path, {"profiling": {"max_memory_mb": 100}})
22
+ """
23
+
24
+ from .fixture import ProfilingFixture
25
+ from .profiler import PerformanceProfiler
26
+
27
+ __all__ = [
28
+ "PerformanceProfiler",
29
+ "ProfilingFixture",
30
+ ]