apte 0.3.0__tar.gz → 0.3.2__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 → apte-0.3.2}/PKG-INFO +8 -8
- {apte-0.3.0 → apte-0.3.2}/README.md +4 -4
- {apte-0.3.0 → apte-0.3.2}/apte/__init__.py +1 -1
- {apte-0.3.0 → apte-0.3.2}/apte/core/execution/parallel.py +10 -2
- {apte-0.3.0 → apte-0.3.2}/apte/events/bus.py +57 -22
- {apte-0.3.0 → apte-0.3.2}/apte.egg-info/PKG-INFO +8 -8
- {apte-0.3.0 → apte-0.3.2}/apte.egg-info/requires.txt +1 -1
- {apte-0.3.0 → apte-0.3.2}/pyproject.toml +12 -3
- {apte-0.3.0 → apte-0.3.2}/LICENSE +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/__main__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/api.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/assertions.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/cache/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/cache/plugin.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/cache/storage.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/cli/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/cli/main.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/compat.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/console.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/collector.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/execution/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/execution/suite_manager.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/execution/test_executor.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/fixture.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/outcome.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/runner.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/session.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/suite.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/core/tracker.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/container.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/decorators.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/factory.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/hashable.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/hints.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/markers.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/proxy.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/di/validation.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/core.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/events.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/log_capture.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/retry.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/skip.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/suite_path.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/entities/xfail.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/evaluator.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/evaluators.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/hashing.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/results_writer.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/suite.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/types.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/evals/wrapper.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/events/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/events/types.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/exceptions.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/execution/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/execution/async_bridge.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/execution/capture.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/execution/context.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/execution/interrupt.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/execution/runner.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/filters/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/filters/keyword.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/filters/kind.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/filters/suite.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/fixtures/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/fixtures/builtins.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/fixtures/mocker.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/history/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/history/collector.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/history/plugin.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/history/storage.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/loader.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/plugin.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/py.typed +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/ascii.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/ctrf.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/factory.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/format.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/log_file.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/rich_reporter.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/verbosity.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/reporting/web.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/shell.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/tags/__init__.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/tags/plugin.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte/utils.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte.egg-info/SOURCES.txt +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte.egg-info/dependency_links.txt +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte.egg-info/entry_points.txt +0 -0
- {apte-0.3.0 → apte-0.3.2}/apte.egg-info/top_level.txt +0 -0
- {apte-0.3.0 → apte-0.3.2}/setup.cfg +0 -0
- {apte-0.3.0 → apte-0.3.2}/tests/test_assertions.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/tests/test_console_print.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/tests/test_normalizers.py +0 -0
- {apte-0.3.0 → apte-0.3.2}/tests/test_shell.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apte
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Async-first Python testing framework
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Async-first Python testing framework with explicit dependency injection and native LLM evals
|
|
5
5
|
Author-email: Renaud Cepre <renaudcepre@protonmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/renaudcepre/apte
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://renaudcepre.github.io/apte/
|
|
|
9
9
|
Project-URL: Repository, https://github.com/renaudcepre/apte
|
|
10
10
|
Project-URL: Changelog, https://github.com/renaudcepre/apte/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Issues, https://github.com/renaudcepre/apte/issues
|
|
12
|
-
Keywords: testing,async,dependency-injection,test-framework,asyncio,fixtures
|
|
12
|
+
Keywords: testing,async,dependency-injection,test-framework,asyncio,fixtures,llm-evals,llm-evaluation,evals,pytest-alternative
|
|
13
13
|
Classifier: Development Status :: 3 - Alpha
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -22,7 +22,7 @@ Classifier: Framework :: AsyncIO
|
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: typing-extensions>=4.
|
|
25
|
+
Requires-Dist: typing-extensions>=4.16.0
|
|
26
26
|
Provides-Extra: rich
|
|
27
27
|
Requires-Dist: rich>=15.0.0; extra == "rich"
|
|
28
28
|
Provides-Extra: web
|
|
@@ -41,6 +41,8 @@ An eval is just a test that returns a value - scored, not asserted. Your evals g
|
|
|
41
41
|
real fixtures, dependency injection and parallelism, and live right next to the
|
|
42
42
|
tests they ship with. Python 3.10+, installs lean (Rich UI optional).
|
|
43
43
|
|
|
44
|
+

|
|
45
|
+
|
|
44
46
|
---
|
|
45
47
|
|
|
46
48
|
## Why Apte?
|
|
@@ -149,14 +151,12 @@ apte run test_sample:session
|
|
|
149
151
|
|
|
150
152
|
## Installation
|
|
151
153
|
|
|
152
|
-
Apte is not yet on PyPI. Install directly from GitHub:
|
|
153
|
-
|
|
154
154
|
```bash
|
|
155
155
|
# With uv (recommended)
|
|
156
|
-
uv add
|
|
156
|
+
uv add apte
|
|
157
157
|
|
|
158
158
|
# With pip
|
|
159
|
-
pip install
|
|
159
|
+
pip install apte
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
## CLI
|
|
@@ -10,6 +10,8 @@ An eval is just a test that returns a value - scored, not asserted. Your evals g
|
|
|
10
10
|
real fixtures, dependency injection and parallelism, and live right next to the
|
|
11
11
|
tests they ship with. Python 3.10+, installs lean (Rich UI optional).
|
|
12
12
|
|
|
13
|
+

|
|
14
|
+
|
|
13
15
|
---
|
|
14
16
|
|
|
15
17
|
## Why Apte?
|
|
@@ -118,14 +120,12 @@ apte run test_sample:session
|
|
|
118
120
|
|
|
119
121
|
## Installation
|
|
120
122
|
|
|
121
|
-
Apte is not yet on PyPI. Install directly from GitHub:
|
|
122
|
-
|
|
123
123
|
```bash
|
|
124
124
|
# With uv (recommended)
|
|
125
|
-
uv add
|
|
125
|
+
uv add apte
|
|
126
126
|
|
|
127
127
|
# With pip
|
|
128
|
-
pip install
|
|
128
|
+
pip install apte
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
## CLI
|
|
@@ -175,8 +175,16 @@ class ParallelExecutor:
|
|
|
175
175
|
ctx.queue.task_done()
|
|
176
176
|
|
|
177
177
|
def _should_stop(self, exitfirst_flag: asyncio.Event | None) -> bool:
|
|
178
|
-
"""Check if execution should stop (interrupt or exitfirst).
|
|
179
|
-
|
|
178
|
+
"""Check if execution should stop (interrupt or exitfirst).
|
|
179
|
+
|
|
180
|
+
Reads the synchronous interrupt state, not soft_stop_event: the event is
|
|
181
|
+
armed via call_soon_threadsafe (one loop iteration late), but _state is
|
|
182
|
+
set the instant the signal handler runs. Between two tests the scheduler
|
|
183
|
+
can reach this check without yielding to the loop (sync handlers run
|
|
184
|
+
inline, so emit() never suspends), so the event may still be unset while
|
|
185
|
+
a stop is already pending.
|
|
186
|
+
"""
|
|
187
|
+
return self._interrupt_handler.should_stop_new_tests or (
|
|
180
188
|
exitfirst_flag is not None and exitfirst_flag.is_set()
|
|
181
189
|
)
|
|
182
190
|
|
|
@@ -26,12 +26,22 @@ class _RegisteredHandler:
|
|
|
26
26
|
|
|
27
27
|
func: Callable[..., Any]
|
|
28
28
|
name: str
|
|
29
|
+
blocking: bool = False
|
|
30
|
+
# Computed once at registration: a handler's async-ness never changes, and
|
|
31
|
+
# asyncio.iscoroutinefunction is costly (unwraps partials, inspects flags).
|
|
32
|
+
is_async: bool = False
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
class EventBus:
|
|
32
36
|
"""Decoupled event dispatch with async support.
|
|
33
37
|
|
|
34
|
-
Sync handlers
|
|
38
|
+
Sync handlers run inline on the event loop by default: they are expected to
|
|
39
|
+
be fast and non-blocking (reporters buffering, result collection). Inline
|
|
40
|
+
dispatch avoids a thread-pool round-trip (socket + loop wakeup) per call,
|
|
41
|
+
which otherwise dominates the per-test cost. Handlers that genuinely block
|
|
42
|
+
(heavy disk/network work) opt in with `on(..., blocking=True)` to be
|
|
43
|
+
offloaded to the thread pool instead.
|
|
44
|
+
|
|
35
45
|
Async handlers run fire-and-forget and are tracked for cleanup.
|
|
36
46
|
|
|
37
47
|
Use wait_pending() before session end to ensure all async handlers complete.
|
|
@@ -84,10 +94,24 @@ class EventBus:
|
|
|
84
94
|
else:
|
|
85
95
|
handler()
|
|
86
96
|
|
|
87
|
-
def on(
|
|
88
|
-
|
|
97
|
+
def on(
|
|
98
|
+
self, event: Event, handler: Callable[..., Any], *, blocking: bool = False
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Register a handler for an event.
|
|
101
|
+
|
|
102
|
+
Sync handlers run inline by default. Pass `blocking=True` for sync
|
|
103
|
+
handlers that perform heavy blocking work and must not stall the loop;
|
|
104
|
+
those are offloaded to the thread pool.
|
|
105
|
+
"""
|
|
89
106
|
name = get_callable_name(handler)
|
|
90
|
-
self._handlers[event].append(
|
|
107
|
+
self._handlers[event].append(
|
|
108
|
+
_RegisteredHandler(
|
|
109
|
+
func=handler,
|
|
110
|
+
name=name,
|
|
111
|
+
blocking=blocking,
|
|
112
|
+
is_async=asyncio.iscoroutinefunction(handler),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
91
115
|
|
|
92
116
|
def off(self, event: Event, handler: Callable[..., Any]) -> None:
|
|
93
117
|
"""Unregister a handler for an event."""
|
|
@@ -105,7 +129,7 @@ class EventBus:
|
|
|
105
129
|
for handler_entry in self._handlers[event]:
|
|
106
130
|
handler = handler_entry.func
|
|
107
131
|
handler_name = handler_entry.name
|
|
108
|
-
is_async =
|
|
132
|
+
is_async = handler_entry.is_async
|
|
109
133
|
|
|
110
134
|
await self._emit_handler_start(handler_name, event, is_async)
|
|
111
135
|
start_time = time.perf_counter()
|
|
@@ -119,16 +143,21 @@ class EventBus:
|
|
|
119
143
|
)
|
|
120
144
|
self._pending_tasks.add(task)
|
|
121
145
|
task.add_done_callback(self._pending_tasks.discard)
|
|
122
|
-
|
|
146
|
+
elif handler_entry.blocking:
|
|
147
|
+
# Opt-in: offload genuinely blocking handlers to a thread,
|
|
148
|
+
# serialized per owner to protect shared instance state.
|
|
123
149
|
lock = self._get_owner_lock(handler)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
await run_in_threadpool(
|
|
151
|
+
self._run_sync_with_lock, lock, handler, data
|
|
152
|
+
)
|
|
153
|
+
duration = time.perf_counter() - start_time
|
|
154
|
+
await self._emit_handler_end(
|
|
155
|
+
handler_name, event, False, duration, None
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
# Default: run inline. No thread, so no cross-thread race and
|
|
159
|
+
# no lock needed; the call runs to completion on the loop.
|
|
160
|
+
self._run_sync_with_lock(None, handler, data)
|
|
132
161
|
duration = time.perf_counter() - start_time
|
|
133
162
|
await self._emit_handler_end(
|
|
134
163
|
handler_name, event, False, duration, None
|
|
@@ -170,13 +199,16 @@ class EventBus:
|
|
|
170
199
|
self, name: str, event: Event, is_async: bool
|
|
171
200
|
) -> None:
|
|
172
201
|
"""Emit HANDLER_START without triggering handler events (avoid recursion)."""
|
|
202
|
+
listeners = self._handlers[Event.HANDLER_START]
|
|
203
|
+
if not listeners:
|
|
204
|
+
return
|
|
173
205
|
info = HandlerInfo(name=name, event=event, is_async=is_async)
|
|
174
|
-
for handler_entry in
|
|
206
|
+
for handler_entry in listeners:
|
|
175
207
|
try:
|
|
176
|
-
if
|
|
208
|
+
if handler_entry.is_async:
|
|
177
209
|
await handler_entry.func(info)
|
|
178
210
|
else:
|
|
179
|
-
|
|
211
|
+
handler_entry.func(info)
|
|
180
212
|
except Exception:
|
|
181
213
|
logger.exception("HANDLER_START listener %s failed", handler_entry.name)
|
|
182
214
|
|
|
@@ -189,15 +221,18 @@ class EventBus:
|
|
|
189
221
|
error: Exception | None,
|
|
190
222
|
) -> None:
|
|
191
223
|
"""Emit HANDLER_END without triggering handler events (avoid recursion)."""
|
|
224
|
+
listeners = self._handlers[Event.HANDLER_END]
|
|
225
|
+
if not listeners:
|
|
226
|
+
return
|
|
192
227
|
info = HandlerInfo(
|
|
193
228
|
name=name, event=event, is_async=is_async, duration=duration, error=error
|
|
194
229
|
)
|
|
195
|
-
for handler_entry in
|
|
230
|
+
for handler_entry in listeners:
|
|
196
231
|
try:
|
|
197
|
-
if
|
|
232
|
+
if handler_entry.is_async:
|
|
198
233
|
await handler_entry.func(info)
|
|
199
234
|
else:
|
|
200
|
-
|
|
235
|
+
handler_entry.func(info)
|
|
201
236
|
except Exception:
|
|
202
237
|
logger.exception("HANDLER_END listener %s failed", handler_entry.name)
|
|
203
238
|
|
|
@@ -216,10 +251,10 @@ class EventBus:
|
|
|
216
251
|
for handler_entry in self._handlers[event]:
|
|
217
252
|
handler = handler_entry.func
|
|
218
253
|
try:
|
|
219
|
-
if
|
|
254
|
+
if handler_entry.is_async:
|
|
220
255
|
result = await handler(data)
|
|
221
256
|
else:
|
|
222
|
-
result =
|
|
257
|
+
result = handler(data)
|
|
223
258
|
if result is not None:
|
|
224
259
|
data = result
|
|
225
260
|
except Exception:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apte
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Async-first Python testing framework
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Async-first Python testing framework with explicit dependency injection and native LLM evals
|
|
5
5
|
Author-email: Renaud Cepre <renaudcepre@protonmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/renaudcepre/apte
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://renaudcepre.github.io/apte/
|
|
|
9
9
|
Project-URL: Repository, https://github.com/renaudcepre/apte
|
|
10
10
|
Project-URL: Changelog, https://github.com/renaudcepre/apte/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Issues, https://github.com/renaudcepre/apte/issues
|
|
12
|
-
Keywords: testing,async,dependency-injection,test-framework,asyncio,fixtures
|
|
12
|
+
Keywords: testing,async,dependency-injection,test-framework,asyncio,fixtures,llm-evals,llm-evaluation,evals,pytest-alternative
|
|
13
13
|
Classifier: Development Status :: 3 - Alpha
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -22,7 +22,7 @@ Classifier: Framework :: AsyncIO
|
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: typing-extensions>=4.
|
|
25
|
+
Requires-Dist: typing-extensions>=4.16.0
|
|
26
26
|
Provides-Extra: rich
|
|
27
27
|
Requires-Dist: rich>=15.0.0; extra == "rich"
|
|
28
28
|
Provides-Extra: web
|
|
@@ -41,6 +41,8 @@ An eval is just a test that returns a value - scored, not asserted. Your evals g
|
|
|
41
41
|
real fixtures, dependency injection and parallelism, and live right next to the
|
|
42
42
|
tests they ship with. Python 3.10+, installs lean (Rich UI optional).
|
|
43
43
|
|
|
44
|
+

|
|
45
|
+
|
|
44
46
|
---
|
|
45
47
|
|
|
46
48
|
## Why Apte?
|
|
@@ -149,14 +151,12 @@ apte run test_sample:session
|
|
|
149
151
|
|
|
150
152
|
## Installation
|
|
151
153
|
|
|
152
|
-
Apte is not yet on PyPI. Install directly from GitHub:
|
|
153
|
-
|
|
154
154
|
```bash
|
|
155
155
|
# With uv (recommended)
|
|
156
|
-
uv add
|
|
156
|
+
uv add apte
|
|
157
157
|
|
|
158
158
|
# With pip
|
|
159
|
-
pip install
|
|
159
|
+
pip install apte
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
## CLI
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apte"
|
|
7
|
-
version = "0.3.
|
|
8
|
-
description = "Async-first Python testing framework"
|
|
7
|
+
version = "0.3.2"
|
|
8
|
+
description = "Async-first Python testing framework with explicit dependency injection and native LLM evals"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
@@ -19,6 +19,10 @@ keywords = [
|
|
|
19
19
|
"test-framework",
|
|
20
20
|
"asyncio",
|
|
21
21
|
"fixtures",
|
|
22
|
+
"llm-evals",
|
|
23
|
+
"llm-evaluation",
|
|
24
|
+
"evals",
|
|
25
|
+
"pytest-alternative",
|
|
22
26
|
]
|
|
23
27
|
classifiers = [
|
|
24
28
|
"Development Status :: 3 - Alpha",
|
|
@@ -32,7 +36,7 @@ classifiers = [
|
|
|
32
36
|
"Framework :: AsyncIO",
|
|
33
37
|
]
|
|
34
38
|
dependencies = [
|
|
35
|
-
"typing-extensions>=4.
|
|
39
|
+
"typing-extensions>=4.16.0",
|
|
36
40
|
]
|
|
37
41
|
|
|
38
42
|
[project.urls]
|
|
@@ -56,6 +60,11 @@ output-format = "concise"
|
|
|
56
60
|
line-length = 88
|
|
57
61
|
extend-exclude = []
|
|
58
62
|
|
|
63
|
+
[tool.ruff.format]
|
|
64
|
+
# ruff >= 0.16 formats Python code blocks inside Markdown by default and
|
|
65
|
+
# inserts PEP 8 blank lines in every doc snippet. Docs stay hand-formatted.
|
|
66
|
+
exclude = ["*.md"]
|
|
67
|
+
|
|
59
68
|
[tool.ruff.lint]
|
|
60
69
|
select = [
|
|
61
70
|
"F", # Pyflakes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|