apte 0.3.0__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.
- apte-0.3.0/LICENSE +21 -0
- apte-0.3.0/PKG-INFO +211 -0
- apte-0.3.0/README.md +180 -0
- apte-0.3.0/apte/__init__.py +55 -0
- apte-0.3.0/apte/__main__.py +5 -0
- apte-0.3.0/apte/api.py +194 -0
- apte-0.3.0/apte/assertions.py +152 -0
- apte-0.3.0/apte/cache/__init__.py +6 -0
- apte-0.3.0/apte/cache/plugin.py +94 -0
- apte-0.3.0/apte/cache/storage.py +132 -0
- apte-0.3.0/apte/cli/__init__.py +0 -0
- apte-0.3.0/apte/cli/main.py +342 -0
- apte-0.3.0/apte/compat.py +15 -0
- apte-0.3.0/apte/console.py +85 -0
- apte-0.3.0/apte/core/__init__.py +0 -0
- apte-0.3.0/apte/core/collector.py +242 -0
- apte-0.3.0/apte/core/execution/__init__.py +7 -0
- apte-0.3.0/apte/core/execution/parallel.py +304 -0
- apte-0.3.0/apte/core/execution/suite_manager.py +93 -0
- apte-0.3.0/apte/core/execution/test_executor.py +371 -0
- apte-0.3.0/apte/core/fixture.py +14 -0
- apte-0.3.0/apte/core/outcome.py +137 -0
- apte-0.3.0/apte/core/runner.py +206 -0
- apte-0.3.0/apte/core/session.py +382 -0
- apte-0.3.0/apte/core/suite.py +236 -0
- apte-0.3.0/apte/core/tracker.py +50 -0
- apte-0.3.0/apte/di/__init__.py +0 -0
- apte-0.3.0/apte/di/container.py +851 -0
- apte-0.3.0/apte/di/decorators.py +220 -0
- apte-0.3.0/apte/di/factory.py +79 -0
- apte-0.3.0/apte/di/hashable.py +57 -0
- apte-0.3.0/apte/di/hints.py +163 -0
- apte-0.3.0/apte/di/markers.py +79 -0
- apte-0.3.0/apte/di/proxy.py +81 -0
- apte-0.3.0/apte/di/validation.py +38 -0
- apte-0.3.0/apte/entities/__init__.py +70 -0
- apte-0.3.0/apte/entities/core.py +158 -0
- apte-0.3.0/apte/entities/events.py +171 -0
- apte-0.3.0/apte/entities/log_capture.py +28 -0
- apte-0.3.0/apte/entities/retry.py +31 -0
- apte-0.3.0/apte/entities/skip.py +63 -0
- apte-0.3.0/apte/entities/suite_path.py +70 -0
- apte-0.3.0/apte/entities/xfail.py +24 -0
- apte-0.3.0/apte/evals/__init__.py +45 -0
- apte-0.3.0/apte/evals/evaluator.py +420 -0
- apte-0.3.0/apte/evals/evaluators.py +199 -0
- apte-0.3.0/apte/evals/hashing.py +109 -0
- apte-0.3.0/apte/evals/results_writer.py +175 -0
- apte-0.3.0/apte/evals/suite.py +98 -0
- apte-0.3.0/apte/evals/types.py +356 -0
- apte-0.3.0/apte/evals/wrapper.py +309 -0
- apte-0.3.0/apte/events/__init__.py +0 -0
- apte-0.3.0/apte/events/bus.py +231 -0
- apte-0.3.0/apte/events/types.py +38 -0
- apte-0.3.0/apte/exceptions.py +188 -0
- apte-0.3.0/apte/execution/__init__.py +0 -0
- apte-0.3.0/apte/execution/async_bridge.py +36 -0
- apte-0.3.0/apte/execution/capture.py +264 -0
- apte-0.3.0/apte/execution/context.py +73 -0
- apte-0.3.0/apte/execution/interrupt.py +118 -0
- apte-0.3.0/apte/execution/runner.py +0 -0
- apte-0.3.0/apte/filters/__init__.py +4 -0
- apte-0.3.0/apte/filters/keyword.py +52 -0
- apte-0.3.0/apte/filters/kind.py +37 -0
- apte-0.3.0/apte/filters/suite.py +43 -0
- apte-0.3.0/apte/fixtures/__init__.py +0 -0
- apte-0.3.0/apte/fixtures/builtins.py +38 -0
- apte-0.3.0/apte/fixtures/mocker.py +145 -0
- apte-0.3.0/apte/history/__init__.py +17 -0
- apte-0.3.0/apte/history/collector.py +80 -0
- apte-0.3.0/apte/history/plugin.py +254 -0
- apte-0.3.0/apte/history/storage.py +295 -0
- apte-0.3.0/apte/loader.py +85 -0
- apte-0.3.0/apte/plugin.py +221 -0
- apte-0.3.0/apte/py.typed +0 -0
- apte-0.3.0/apte/reporting/__init__.py +10 -0
- apte-0.3.0/apte/reporting/ascii.py +419 -0
- apte-0.3.0/apte/reporting/ctrf.py +252 -0
- apte-0.3.0/apte/reporting/factory.py +31 -0
- apte-0.3.0/apte/reporting/format.py +39 -0
- apte-0.3.0/apte/reporting/log_file.py +111 -0
- apte-0.3.0/apte/reporting/rich_reporter.py +523 -0
- apte-0.3.0/apte/reporting/verbosity.py +18 -0
- apte-0.3.0/apte/reporting/web.py +347 -0
- apte-0.3.0/apte/shell.py +200 -0
- apte-0.3.0/apte/tags/__init__.py +5 -0
- apte-0.3.0/apte/tags/plugin.py +77 -0
- apte-0.3.0/apte/utils.py +26 -0
- apte-0.3.0/apte.egg-info/PKG-INFO +211 -0
- apte-0.3.0/apte.egg-info/SOURCES.txt +98 -0
- apte-0.3.0/apte.egg-info/dependency_links.txt +1 -0
- apte-0.3.0/apte.egg-info/entry_points.txt +2 -0
- apte-0.3.0/apte.egg-info/requires.txt +7 -0
- apte-0.3.0/apte.egg-info/top_level.txt +1 -0
- apte-0.3.0/pyproject.toml +179 -0
- apte-0.3.0/setup.cfg +4 -0
- apte-0.3.0/tests/test_assertions.py +141 -0
- apte-0.3.0/tests/test_console_print.py +171 -0
- apte-0.3.0/tests/test_normalizers.py +73 -0
- apte-0.3.0/tests/test_shell.py +252 -0
apte-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Renaud Cepre
|
|
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.
|
apte-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apte
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Async-first Python testing framework
|
|
5
|
+
Author-email: Renaud Cepre <renaudcepre@protonmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/renaudcepre/apte
|
|
8
|
+
Project-URL: Documentation, https://renaudcepre.github.io/apte/
|
|
9
|
+
Project-URL: Repository, https://github.com/renaudcepre/apte
|
|
10
|
+
Project-URL: Changelog, https://github.com/renaudcepre/apte/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Issues, https://github.com/renaudcepre/apte/issues
|
|
12
|
+
Keywords: testing,async,dependency-injection,test-framework,asyncio,fixtures
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Framework :: AsyncIO
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
26
|
+
Provides-Extra: rich
|
|
27
|
+
Requires-Dist: rich>=15.0.0; extra == "rich"
|
|
28
|
+
Provides-Extra: web
|
|
29
|
+
Requires-Dist: websockets>=16.0; extra == "web"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
[](https://github.com/renaudcepre/apte/actions/workflows/ci.yml)
|
|
35
|
+
[](https://codecov.io/gh/renaudcepre/apte)
|
|
36
|
+
[](https://renaudcepre.github.io/apte/)
|
|
37
|
+
|
|
38
|
+
**Write your tests and your LLM evals in one async-first framework.**
|
|
39
|
+
|
|
40
|
+
An eval is just a test that returns a value - scored, not asserted. Your evals get
|
|
41
|
+
real fixtures, dependency injection and parallelism, and live right next to the
|
|
42
|
+
tests they ship with. Python 3.10+, installs lean (Rich UI optional).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Why Apte?
|
|
47
|
+
|
|
48
|
+
### Explicit Injection (IDE-Ready)
|
|
49
|
+
|
|
50
|
+
**Ctrl+Click works.** Your IDE knows every type. No guessing where fixtures come from.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
def test_user(db: Annotated[Database, Use(database)]): ...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Native Async & Parallelism
|
|
57
|
+
|
|
58
|
+
Tests run as coroutines on a single event loop. No plugin needed.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
apte run tests:session -n 10
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Smart Tagging (Tag Propagation)
|
|
65
|
+
|
|
66
|
+
Tag a fixture once, every test using it inherits the tag automatically.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
@fixture(tags=["database"])
|
|
70
|
+
def db(): ...
|
|
71
|
+
|
|
72
|
+
session.bind(db)
|
|
73
|
+
|
|
74
|
+
@session.test()
|
|
75
|
+
def test_users(repo: Annotated[Repo, Use(user_repo)]): ... # Also tagged "database"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
apte run tests:session --no-tag database # Skips ALL tests touching DB
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Infra vs Code Errors (Error ≠ Fail)
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
✗ test_create_user: AssertionError # Your bug - TEST FAILED
|
|
86
|
+
⚠ test_with_db: [FIXTURE] ConnectionError # Infra issue - SETUP ERROR
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Typed Parameterization
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
CODES = ForEach([200, 201])
|
|
93
|
+
|
|
94
|
+
@session.test()
|
|
95
|
+
def test_status(code: Annotated[int, From(CODES)]): ...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Native LLM Evals
|
|
99
|
+
|
|
100
|
+
Score model outputs alongside your tests - same fixtures, same parallelism, same `apte` CLI. Cases get pass/fail + numeric metrics, persisted to JSONL for run-over-run comparison.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from typing import Annotated
|
|
104
|
+
from apte import ForEach, From, ApteSession
|
|
105
|
+
from apte.evals import EvalCase, EvalSuite
|
|
106
|
+
from apte.evals.evaluators import contains_keywords
|
|
107
|
+
|
|
108
|
+
session = ApteSession()
|
|
109
|
+
chatbot_suite = EvalSuite("chatbot")
|
|
110
|
+
session.add_suite(chatbot_suite)
|
|
111
|
+
|
|
112
|
+
cases = ForEach([
|
|
113
|
+
EvalCase(name="capital_fr", inputs="Capital of France?", expected="Paris"),
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
@chatbot_suite.eval(evaluators=[contains_keywords(keywords=["paris"])])
|
|
117
|
+
async def chatbot(case: Annotated[EvalCase, From(cases)]) -> str:
|
|
118
|
+
return await my_agent(case.inputs) # your LLM call
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
apte eval evals.session:session # runs are recorded to .apte/history.jsonl
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
See [Evals docs](https://renaudcepre.github.io/apte/evals/) for evaluators, judges, and scoring.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Quick Start
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from apte import ApteSession
|
|
133
|
+
|
|
134
|
+
session = ApteSession()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def inc(x):
|
|
138
|
+
return x + 1
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@session.test()
|
|
142
|
+
def test_answer():
|
|
143
|
+
assert inc(3) == 4
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
apte run test_sample:session
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Installation
|
|
151
|
+
|
|
152
|
+
Apte is not yet on PyPI. Install directly from GitHub:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# With uv (recommended)
|
|
156
|
+
uv add git+https://github.com/renaudcepre/apte.git
|
|
157
|
+
|
|
158
|
+
# With pip
|
|
159
|
+
pip install git+https://github.com/renaudcepre/apte.git
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## CLI
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
apte run module:session # Run tests
|
|
166
|
+
apte run module:session -n 4 # Parallel (4 workers)
|
|
167
|
+
apte run module:session --lf # Re-run failed tests only
|
|
168
|
+
apte run module:session --collect-only # List tests without running
|
|
169
|
+
apte run module:session --cache-clear # Clear cache before run
|
|
170
|
+
apte run module:session --app-dir src # Look for module in src/
|
|
171
|
+
apte run module:session --ctrf-output r.json # CTRF report for CI/CD
|
|
172
|
+
|
|
173
|
+
apte eval module:session # Run LLM evals
|
|
174
|
+
apte eval module:session --tag safety # Filter by case tag
|
|
175
|
+
apte eval module:session --last-failed # Re-run failed cases only
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Features
|
|
179
|
+
|
|
180
|
+
- **Explicit DI** - No guessing which fixture you're using
|
|
181
|
+
- **Async native** - No plugin needed, just `async def`
|
|
182
|
+
- **Parallel execution** - Built-in with `-n 4`
|
|
183
|
+
- **Scoped fixtures** - `SESSION`, `SUITE`, `TEST`
|
|
184
|
+
- **Mix sync/async** - They just work together
|
|
185
|
+
- **Factory fixtures** - Callables to create instances on-demand
|
|
186
|
+
- **Plugin system** - Custom reporters, filters
|
|
187
|
+
- **Last-failed mode** - Re-run only failed tests with `--lf`
|
|
188
|
+
- **CTRF reports** - Standardized JSON for CI/CD integration
|
|
189
|
+
- **Native LLM evals** - Scored cases, JSONL history, `apte eval` (see [evals docs](https://renaudcepre.github.io/apte/evals/))
|
|
190
|
+
|
|
191
|
+
## Why Not pytest?
|
|
192
|
+
|
|
193
|
+
| | pytest | Apte |
|
|
194
|
+
|----------|---------------------------------|--------------------------------------|
|
|
195
|
+
| Fixtures | Implicit (by name) | Explicit (`Use(fixture)`) |
|
|
196
|
+
| Params | Hidden in fixture | Visible in test (`From()` + factory) |
|
|
197
|
+
| Async | Plugin required | Native |
|
|
198
|
+
| Parallel | Plugin required | Built-in |
|
|
199
|
+
| Cycles | Runtime error | Prevented at registration |
|
|
200
|
+
| Evals | External (deepeval, pydantic-…) | Native (`apte eval`, JSONL history) |
|
|
201
|
+
|
|
202
|
+
pytest has a large ecosystem and extensive community. Apte is an alternative if you
|
|
203
|
+
prefer FastAPI-style explicit dependencies and native async in your tests.
|
|
204
|
+
|
|
205
|
+
## Documentation
|
|
206
|
+
|
|
207
|
+
Full API reference, guides, and examples: [renaudcepre.github.io/apte](https://renaudcepre.github.io/apte/)
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
apte-0.3.0/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://github.com/renaudcepre/apte/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/renaudcepre/apte)
|
|
5
|
+
[](https://renaudcepre.github.io/apte/)
|
|
6
|
+
|
|
7
|
+
**Write your tests and your LLM evals in one async-first framework.**
|
|
8
|
+
|
|
9
|
+
An eval is just a test that returns a value - scored, not asserted. Your evals get
|
|
10
|
+
real fixtures, dependency injection and parallelism, and live right next to the
|
|
11
|
+
tests they ship with. Python 3.10+, installs lean (Rich UI optional).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why Apte?
|
|
16
|
+
|
|
17
|
+
### Explicit Injection (IDE-Ready)
|
|
18
|
+
|
|
19
|
+
**Ctrl+Click works.** Your IDE knows every type. No guessing where fixtures come from.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
def test_user(db: Annotated[Database, Use(database)]): ...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Native Async & Parallelism
|
|
26
|
+
|
|
27
|
+
Tests run as coroutines on a single event loop. No plugin needed.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
apte run tests:session -n 10
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Smart Tagging (Tag Propagation)
|
|
34
|
+
|
|
35
|
+
Tag a fixture once, every test using it inherits the tag automatically.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
@fixture(tags=["database"])
|
|
39
|
+
def db(): ...
|
|
40
|
+
|
|
41
|
+
session.bind(db)
|
|
42
|
+
|
|
43
|
+
@session.test()
|
|
44
|
+
def test_users(repo: Annotated[Repo, Use(user_repo)]): ... # Also tagged "database"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
apte run tests:session --no-tag database # Skips ALL tests touching DB
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Infra vs Code Errors (Error ≠ Fail)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
✗ test_create_user: AssertionError # Your bug - TEST FAILED
|
|
55
|
+
⚠ test_with_db: [FIXTURE] ConnectionError # Infra issue - SETUP ERROR
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Typed Parameterization
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
CODES = ForEach([200, 201])
|
|
62
|
+
|
|
63
|
+
@session.test()
|
|
64
|
+
def test_status(code: Annotated[int, From(CODES)]): ...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Native LLM Evals
|
|
68
|
+
|
|
69
|
+
Score model outputs alongside your tests - same fixtures, same parallelism, same `apte` CLI. Cases get pass/fail + numeric metrics, persisted to JSONL for run-over-run comparison.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from typing import Annotated
|
|
73
|
+
from apte import ForEach, From, ApteSession
|
|
74
|
+
from apte.evals import EvalCase, EvalSuite
|
|
75
|
+
from apte.evals.evaluators import contains_keywords
|
|
76
|
+
|
|
77
|
+
session = ApteSession()
|
|
78
|
+
chatbot_suite = EvalSuite("chatbot")
|
|
79
|
+
session.add_suite(chatbot_suite)
|
|
80
|
+
|
|
81
|
+
cases = ForEach([
|
|
82
|
+
EvalCase(name="capital_fr", inputs="Capital of France?", expected="Paris"),
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
@chatbot_suite.eval(evaluators=[contains_keywords(keywords=["paris"])])
|
|
86
|
+
async def chatbot(case: Annotated[EvalCase, From(cases)]) -> str:
|
|
87
|
+
return await my_agent(case.inputs) # your LLM call
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
apte eval evals.session:session # runs are recorded to .apte/history.jsonl
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [Evals docs](https://renaudcepre.github.io/apte/evals/) for evaluators, judges, and scoring.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Quick Start
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from apte import ApteSession
|
|
102
|
+
|
|
103
|
+
session = ApteSession()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def inc(x):
|
|
107
|
+
return x + 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@session.test()
|
|
111
|
+
def test_answer():
|
|
112
|
+
assert inc(3) == 4
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
apte run test_sample:session
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Installation
|
|
120
|
+
|
|
121
|
+
Apte is not yet on PyPI. Install directly from GitHub:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# With uv (recommended)
|
|
125
|
+
uv add git+https://github.com/renaudcepre/apte.git
|
|
126
|
+
|
|
127
|
+
# With pip
|
|
128
|
+
pip install git+https://github.com/renaudcepre/apte.git
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## CLI
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
apte run module:session # Run tests
|
|
135
|
+
apte run module:session -n 4 # Parallel (4 workers)
|
|
136
|
+
apte run module:session --lf # Re-run failed tests only
|
|
137
|
+
apte run module:session --collect-only # List tests without running
|
|
138
|
+
apte run module:session --cache-clear # Clear cache before run
|
|
139
|
+
apte run module:session --app-dir src # Look for module in src/
|
|
140
|
+
apte run module:session --ctrf-output r.json # CTRF report for CI/CD
|
|
141
|
+
|
|
142
|
+
apte eval module:session # Run LLM evals
|
|
143
|
+
apte eval module:session --tag safety # Filter by case tag
|
|
144
|
+
apte eval module:session --last-failed # Re-run failed cases only
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Features
|
|
148
|
+
|
|
149
|
+
- **Explicit DI** - No guessing which fixture you're using
|
|
150
|
+
- **Async native** - No plugin needed, just `async def`
|
|
151
|
+
- **Parallel execution** - Built-in with `-n 4`
|
|
152
|
+
- **Scoped fixtures** - `SESSION`, `SUITE`, `TEST`
|
|
153
|
+
- **Mix sync/async** - They just work together
|
|
154
|
+
- **Factory fixtures** - Callables to create instances on-demand
|
|
155
|
+
- **Plugin system** - Custom reporters, filters
|
|
156
|
+
- **Last-failed mode** - Re-run only failed tests with `--lf`
|
|
157
|
+
- **CTRF reports** - Standardized JSON for CI/CD integration
|
|
158
|
+
- **Native LLM evals** - Scored cases, JSONL history, `apte eval` (see [evals docs](https://renaudcepre.github.io/apte/evals/))
|
|
159
|
+
|
|
160
|
+
## Why Not pytest?
|
|
161
|
+
|
|
162
|
+
| | pytest | Apte |
|
|
163
|
+
|----------|---------------------------------|--------------------------------------|
|
|
164
|
+
| Fixtures | Implicit (by name) | Explicit (`Use(fixture)`) |
|
|
165
|
+
| Params | Hidden in fixture | Visible in test (`From()` + factory) |
|
|
166
|
+
| Async | Plugin required | Native |
|
|
167
|
+
| Parallel | Plugin required | Built-in |
|
|
168
|
+
| Cycles | Runtime error | Prevented at registration |
|
|
169
|
+
| Evals | External (deepeval, pydantic-…) | Native (`apte eval`, JSONL history) |
|
|
170
|
+
|
|
171
|
+
pytest has a large ecosystem and extensive community. Apte is an alternative if you
|
|
172
|
+
prefer FastAPI-style explicit dependencies and native async in your tests.
|
|
173
|
+
|
|
174
|
+
## Documentation
|
|
175
|
+
|
|
176
|
+
Full API reference, guides, and examples: [renaudcepre.github.io/apte](https://renaudcepre.github.io/apte/)
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from apte import console
|
|
2
|
+
from apte.api import collect_tests, list_tags, run_session
|
|
3
|
+
from apte.assertions import ExceptionInfo, RaisesContext, raises, warns
|
|
4
|
+
from apte.core.session import ApteSession
|
|
5
|
+
from apte.core.suite import ApteSuite
|
|
6
|
+
from apte.di.decorators import factory, fixture
|
|
7
|
+
from apte.di.factory import FixtureFactory
|
|
8
|
+
from apte.di.markers import ForEach, From, Use
|
|
9
|
+
from apte.entities import FixtureCallable, Retry, Skip, Xfail
|
|
10
|
+
from apte.exceptions import ApteError, CircularDependencyError, FixtureError
|
|
11
|
+
from apte.fixtures.builtins import caplog, mocker, tmp_path
|
|
12
|
+
from apte.fixtures.mocker import AsyncMockType, Mocker, MockType
|
|
13
|
+
from apte.loader import LoadError, load_session
|
|
14
|
+
from apte.plugin import PluginBase
|
|
15
|
+
from apte.shell import CommandResult, Shell
|
|
16
|
+
|
|
17
|
+
__version__ = "0.3.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ApteError",
|
|
21
|
+
"ApteSession",
|
|
22
|
+
"ApteSuite",
|
|
23
|
+
"AsyncMockType",
|
|
24
|
+
"CircularDependencyError",
|
|
25
|
+
"CommandResult",
|
|
26
|
+
"ExceptionInfo",
|
|
27
|
+
"FixtureCallable",
|
|
28
|
+
"FixtureError",
|
|
29
|
+
"FixtureFactory",
|
|
30
|
+
"ForEach",
|
|
31
|
+
"From",
|
|
32
|
+
"LoadError",
|
|
33
|
+
"MockType",
|
|
34
|
+
"Mocker",
|
|
35
|
+
"PluginBase",
|
|
36
|
+
"RaisesContext",
|
|
37
|
+
"Retry",
|
|
38
|
+
"Shell",
|
|
39
|
+
"Skip",
|
|
40
|
+
"Use",
|
|
41
|
+
"Xfail",
|
|
42
|
+
"__version__",
|
|
43
|
+
"caplog",
|
|
44
|
+
"collect_tests",
|
|
45
|
+
"console",
|
|
46
|
+
"factory",
|
|
47
|
+
"fixture",
|
|
48
|
+
"list_tags",
|
|
49
|
+
"load_session",
|
|
50
|
+
"mocker",
|
|
51
|
+
"raises",
|
|
52
|
+
"run_session",
|
|
53
|
+
"tmp_path",
|
|
54
|
+
"warns",
|
|
55
|
+
]
|
apte-0.3.0/apte/api.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Public API for running Apte sessions.
|
|
2
|
+
|
|
3
|
+
This module provides the main entry points for running tests programmatically,
|
|
4
|
+
independent of any CLI or adapter implementation.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from apte import ApteSession
|
|
8
|
+
from apte.api import run_session
|
|
9
|
+
|
|
10
|
+
session = ApteSession()
|
|
11
|
+
|
|
12
|
+
@session.test()
|
|
13
|
+
def test_example():
|
|
14
|
+
assert True
|
|
15
|
+
|
|
16
|
+
success = run_session(session)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from apte.core.collector import Collector
|
|
25
|
+
from apte.core.runner import TestRunner
|
|
26
|
+
from apte.core.suite import (
|
|
27
|
+
ApteSuite, # noqa: TC001 - used at runtime in list_tags
|
|
28
|
+
)
|
|
29
|
+
from apte.events.types import Event
|
|
30
|
+
from apte.filters.keyword import KeywordFilterPlugin
|
|
31
|
+
from apte.filters.kind import KindFilterPlugin
|
|
32
|
+
from apte.filters.suite import SuiteFilterPlugin
|
|
33
|
+
from apte.plugin import PluginBase, PluginContext
|
|
34
|
+
from apte.tags.plugin import TagFilterPlugin
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from apte.core.session import ApteSession
|
|
38
|
+
from apte.entities import RunResult, TestItem
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_session( # noqa: PLR0913 - public API with many optional params
|
|
42
|
+
session: ApteSession,
|
|
43
|
+
concurrency: int | None = None,
|
|
44
|
+
exitfirst: bool = False,
|
|
45
|
+
last_failed: bool = False,
|
|
46
|
+
cache_clear: bool = False,
|
|
47
|
+
include_tags: set[str] | None = None,
|
|
48
|
+
exclude_tags: set[str] | None = None,
|
|
49
|
+
capture: bool = True,
|
|
50
|
+
suite_filter: str | None = None,
|
|
51
|
+
keyword_patterns: list[str] | None = None,
|
|
52
|
+
log_file: bool = True,
|
|
53
|
+
force_no_color: bool = False,
|
|
54
|
+
*,
|
|
55
|
+
ctx: PluginContext | None = None,
|
|
56
|
+
) -> RunResult:
|
|
57
|
+
"""Run a test session and return result with success and interrupted status.
|
|
58
|
+
|
|
59
|
+
This is the main entry point for running tests programmatically.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
session: The ApteSession to run.
|
|
63
|
+
concurrency: Number of concurrent workers (None = use session default).
|
|
64
|
+
exitfirst: Stop after first failure.
|
|
65
|
+
last_failed: Only run tests that failed in the last run.
|
|
66
|
+
cache_clear: Clear the cache before running.
|
|
67
|
+
include_tags: Only run tests with these tags (OR logic).
|
|
68
|
+
exclude_tags: Exclude tests with these tags.
|
|
69
|
+
capture: Capture stdout/stderr during tests (default: True).
|
|
70
|
+
suite_filter: Only run tests in this suite (::SuiteName syntax).
|
|
71
|
+
keyword_patterns: Only run tests matching these patterns (-k flag).
|
|
72
|
+
log_file: Write output to .apte/last_run.log (default: True).
|
|
73
|
+
force_no_color: Disable colors (--no-color flag).
|
|
74
|
+
ctx: Plugin context (if provided, overrides individual params above).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
RunResult with success status and interrupted flag.
|
|
78
|
+
"""
|
|
79
|
+
# Apply session-level settings from ctx or params
|
|
80
|
+
if ctx is not None:
|
|
81
|
+
if ctx.get("concurrency") is not None:
|
|
82
|
+
session.concurrency = ctx.get("concurrency")
|
|
83
|
+
session.exitfirst = ctx.get("exitfirst", False)
|
|
84
|
+
session.capture = not ctx.get("no_capture", False)
|
|
85
|
+
else:
|
|
86
|
+
if concurrency is not None:
|
|
87
|
+
session.concurrency = concurrency
|
|
88
|
+
session.exitfirst = exitfirst
|
|
89
|
+
session.capture = capture
|
|
90
|
+
|
|
91
|
+
# Register default plugins if none registered
|
|
92
|
+
if not session.plugin_classes:
|
|
93
|
+
session.register_default_plugins()
|
|
94
|
+
|
|
95
|
+
# Build context from parameters if not provided
|
|
96
|
+
if ctx is None:
|
|
97
|
+
ctx = PluginContext(
|
|
98
|
+
args={
|
|
99
|
+
"last_failed": last_failed,
|
|
100
|
+
"cache_clear": cache_clear,
|
|
101
|
+
"tags": list(include_tags) if include_tags else [],
|
|
102
|
+
"exclude_tags": list(exclude_tags) if exclude_tags else [],
|
|
103
|
+
"target_suite": suite_filter,
|
|
104
|
+
"keywords": keyword_patterns or [],
|
|
105
|
+
"no_log_file": not log_file,
|
|
106
|
+
"no_color": force_no_color,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
session.activate_plugins(ctx)
|
|
111
|
+
|
|
112
|
+
runner = TestRunner(session)
|
|
113
|
+
return runner.run()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def collect_tests( # noqa: PLR0913 - public API with many optional params
|
|
117
|
+
session: ApteSession,
|
|
118
|
+
include_tags: set[str] | None = None,
|
|
119
|
+
exclude_tags: set[str] | None = None,
|
|
120
|
+
suite_filter: str | None = None,
|
|
121
|
+
keyword_patterns: list[str] | None = None,
|
|
122
|
+
*,
|
|
123
|
+
ctx: PluginContext | None = None,
|
|
124
|
+
) -> list[TestItem]:
|
|
125
|
+
"""Collect tests from a session without running them.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
session: The ApteSession to collect from.
|
|
129
|
+
include_tags: Only include tests with these tags.
|
|
130
|
+
exclude_tags: Exclude tests with these tags.
|
|
131
|
+
suite_filter: Only include tests in this suite.
|
|
132
|
+
keyword_patterns: Only include tests matching these patterns.
|
|
133
|
+
ctx: Plugin context (if provided, overrides individual params above).
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of collected TestItem objects.
|
|
137
|
+
"""
|
|
138
|
+
# Build context from parameters if not provided
|
|
139
|
+
if ctx is None:
|
|
140
|
+
ctx = PluginContext(
|
|
141
|
+
args={
|
|
142
|
+
"tags": list(include_tags) if include_tags else [],
|
|
143
|
+
"exclude_tags": list(exclude_tags) if exclude_tags else [],
|
|
144
|
+
"target_suite": suite_filter,
|
|
145
|
+
"keywords": keyword_patterns or [],
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Activate filter plugins
|
|
150
|
+
filter_plugins: list[type[PluginBase]] = [
|
|
151
|
+
TagFilterPlugin,
|
|
152
|
+
SuiteFilterPlugin,
|
|
153
|
+
KeywordFilterPlugin,
|
|
154
|
+
KindFilterPlugin,
|
|
155
|
+
]
|
|
156
|
+
for plugin_class in filter_plugins:
|
|
157
|
+
instance = plugin_class.activate(ctx)
|
|
158
|
+
if instance is not None:
|
|
159
|
+
session.register_plugin(instance)
|
|
160
|
+
|
|
161
|
+
collector = Collector()
|
|
162
|
+
items = collector.collect(session)
|
|
163
|
+
return asyncio.run(session.events.emit_and_collect(Event.COLLECTION_FINISH, items))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def list_tags(session: ApteSession) -> set[str]:
|
|
167
|
+
"""List all declared tags in a session.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
session: The ApteSession to inspect.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Set of all tag names declared on fixtures, suites, and tests.
|
|
174
|
+
"""
|
|
175
|
+
all_tags: set[str] = set()
|
|
176
|
+
|
|
177
|
+
for fixture_reg in session.fixtures:
|
|
178
|
+
all_tags.update(fixture_reg.tags)
|
|
179
|
+
|
|
180
|
+
for test_reg in session.tests:
|
|
181
|
+
all_tags.update(test_reg.tags)
|
|
182
|
+
|
|
183
|
+
def collect_from_suites(suites: list[ApteSuite]) -> None:
|
|
184
|
+
for suite in suites:
|
|
185
|
+
all_tags.update(suite.tags)
|
|
186
|
+
for fixture_reg in suite.fixtures:
|
|
187
|
+
all_tags.update(fixture_reg.tags)
|
|
188
|
+
for test_reg in suite.tests:
|
|
189
|
+
all_tags.update(test_reg.tags)
|
|
190
|
+
collect_from_suites(suite.suites)
|
|
191
|
+
|
|
192
|
+
collect_from_suites(session.suites)
|
|
193
|
+
|
|
194
|
+
return all_tags
|