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.
Files changed (100) hide show
  1. {apte-0.3.0 → apte-0.3.2}/PKG-INFO +8 -8
  2. {apte-0.3.0 → apte-0.3.2}/README.md +4 -4
  3. {apte-0.3.0 → apte-0.3.2}/apte/__init__.py +1 -1
  4. {apte-0.3.0 → apte-0.3.2}/apte/core/execution/parallel.py +10 -2
  5. {apte-0.3.0 → apte-0.3.2}/apte/events/bus.py +57 -22
  6. {apte-0.3.0 → apte-0.3.2}/apte.egg-info/PKG-INFO +8 -8
  7. {apte-0.3.0 → apte-0.3.2}/apte.egg-info/requires.txt +1 -1
  8. {apte-0.3.0 → apte-0.3.2}/pyproject.toml +12 -3
  9. {apte-0.3.0 → apte-0.3.2}/LICENSE +0 -0
  10. {apte-0.3.0 → apte-0.3.2}/apte/__main__.py +0 -0
  11. {apte-0.3.0 → apte-0.3.2}/apte/api.py +0 -0
  12. {apte-0.3.0 → apte-0.3.2}/apte/assertions.py +0 -0
  13. {apte-0.3.0 → apte-0.3.2}/apte/cache/__init__.py +0 -0
  14. {apte-0.3.0 → apte-0.3.2}/apte/cache/plugin.py +0 -0
  15. {apte-0.3.0 → apte-0.3.2}/apte/cache/storage.py +0 -0
  16. {apte-0.3.0 → apte-0.3.2}/apte/cli/__init__.py +0 -0
  17. {apte-0.3.0 → apte-0.3.2}/apte/cli/main.py +0 -0
  18. {apte-0.3.0 → apte-0.3.2}/apte/compat.py +0 -0
  19. {apte-0.3.0 → apte-0.3.2}/apte/console.py +0 -0
  20. {apte-0.3.0 → apte-0.3.2}/apte/core/__init__.py +0 -0
  21. {apte-0.3.0 → apte-0.3.2}/apte/core/collector.py +0 -0
  22. {apte-0.3.0 → apte-0.3.2}/apte/core/execution/__init__.py +0 -0
  23. {apte-0.3.0 → apte-0.3.2}/apte/core/execution/suite_manager.py +0 -0
  24. {apte-0.3.0 → apte-0.3.2}/apte/core/execution/test_executor.py +0 -0
  25. {apte-0.3.0 → apte-0.3.2}/apte/core/fixture.py +0 -0
  26. {apte-0.3.0 → apte-0.3.2}/apte/core/outcome.py +0 -0
  27. {apte-0.3.0 → apte-0.3.2}/apte/core/runner.py +0 -0
  28. {apte-0.3.0 → apte-0.3.2}/apte/core/session.py +0 -0
  29. {apte-0.3.0 → apte-0.3.2}/apte/core/suite.py +0 -0
  30. {apte-0.3.0 → apte-0.3.2}/apte/core/tracker.py +0 -0
  31. {apte-0.3.0 → apte-0.3.2}/apte/di/__init__.py +0 -0
  32. {apte-0.3.0 → apte-0.3.2}/apte/di/container.py +0 -0
  33. {apte-0.3.0 → apte-0.3.2}/apte/di/decorators.py +0 -0
  34. {apte-0.3.0 → apte-0.3.2}/apte/di/factory.py +0 -0
  35. {apte-0.3.0 → apte-0.3.2}/apte/di/hashable.py +0 -0
  36. {apte-0.3.0 → apte-0.3.2}/apte/di/hints.py +0 -0
  37. {apte-0.3.0 → apte-0.3.2}/apte/di/markers.py +0 -0
  38. {apte-0.3.0 → apte-0.3.2}/apte/di/proxy.py +0 -0
  39. {apte-0.3.0 → apte-0.3.2}/apte/di/validation.py +0 -0
  40. {apte-0.3.0 → apte-0.3.2}/apte/entities/__init__.py +0 -0
  41. {apte-0.3.0 → apte-0.3.2}/apte/entities/core.py +0 -0
  42. {apte-0.3.0 → apte-0.3.2}/apte/entities/events.py +0 -0
  43. {apte-0.3.0 → apte-0.3.2}/apte/entities/log_capture.py +0 -0
  44. {apte-0.3.0 → apte-0.3.2}/apte/entities/retry.py +0 -0
  45. {apte-0.3.0 → apte-0.3.2}/apte/entities/skip.py +0 -0
  46. {apte-0.3.0 → apte-0.3.2}/apte/entities/suite_path.py +0 -0
  47. {apte-0.3.0 → apte-0.3.2}/apte/entities/xfail.py +0 -0
  48. {apte-0.3.0 → apte-0.3.2}/apte/evals/__init__.py +0 -0
  49. {apte-0.3.0 → apte-0.3.2}/apte/evals/evaluator.py +0 -0
  50. {apte-0.3.0 → apte-0.3.2}/apte/evals/evaluators.py +0 -0
  51. {apte-0.3.0 → apte-0.3.2}/apte/evals/hashing.py +0 -0
  52. {apte-0.3.0 → apte-0.3.2}/apte/evals/results_writer.py +0 -0
  53. {apte-0.3.0 → apte-0.3.2}/apte/evals/suite.py +0 -0
  54. {apte-0.3.0 → apte-0.3.2}/apte/evals/types.py +0 -0
  55. {apte-0.3.0 → apte-0.3.2}/apte/evals/wrapper.py +0 -0
  56. {apte-0.3.0 → apte-0.3.2}/apte/events/__init__.py +0 -0
  57. {apte-0.3.0 → apte-0.3.2}/apte/events/types.py +0 -0
  58. {apte-0.3.0 → apte-0.3.2}/apte/exceptions.py +0 -0
  59. {apte-0.3.0 → apte-0.3.2}/apte/execution/__init__.py +0 -0
  60. {apte-0.3.0 → apte-0.3.2}/apte/execution/async_bridge.py +0 -0
  61. {apte-0.3.0 → apte-0.3.2}/apte/execution/capture.py +0 -0
  62. {apte-0.3.0 → apte-0.3.2}/apte/execution/context.py +0 -0
  63. {apte-0.3.0 → apte-0.3.2}/apte/execution/interrupt.py +0 -0
  64. {apte-0.3.0 → apte-0.3.2}/apte/execution/runner.py +0 -0
  65. {apte-0.3.0 → apte-0.3.2}/apte/filters/__init__.py +0 -0
  66. {apte-0.3.0 → apte-0.3.2}/apte/filters/keyword.py +0 -0
  67. {apte-0.3.0 → apte-0.3.2}/apte/filters/kind.py +0 -0
  68. {apte-0.3.0 → apte-0.3.2}/apte/filters/suite.py +0 -0
  69. {apte-0.3.0 → apte-0.3.2}/apte/fixtures/__init__.py +0 -0
  70. {apte-0.3.0 → apte-0.3.2}/apte/fixtures/builtins.py +0 -0
  71. {apte-0.3.0 → apte-0.3.2}/apte/fixtures/mocker.py +0 -0
  72. {apte-0.3.0 → apte-0.3.2}/apte/history/__init__.py +0 -0
  73. {apte-0.3.0 → apte-0.3.2}/apte/history/collector.py +0 -0
  74. {apte-0.3.0 → apte-0.3.2}/apte/history/plugin.py +0 -0
  75. {apte-0.3.0 → apte-0.3.2}/apte/history/storage.py +0 -0
  76. {apte-0.3.0 → apte-0.3.2}/apte/loader.py +0 -0
  77. {apte-0.3.0 → apte-0.3.2}/apte/plugin.py +0 -0
  78. {apte-0.3.0 → apte-0.3.2}/apte/py.typed +0 -0
  79. {apte-0.3.0 → apte-0.3.2}/apte/reporting/__init__.py +0 -0
  80. {apte-0.3.0 → apte-0.3.2}/apte/reporting/ascii.py +0 -0
  81. {apte-0.3.0 → apte-0.3.2}/apte/reporting/ctrf.py +0 -0
  82. {apte-0.3.0 → apte-0.3.2}/apte/reporting/factory.py +0 -0
  83. {apte-0.3.0 → apte-0.3.2}/apte/reporting/format.py +0 -0
  84. {apte-0.3.0 → apte-0.3.2}/apte/reporting/log_file.py +0 -0
  85. {apte-0.3.0 → apte-0.3.2}/apte/reporting/rich_reporter.py +0 -0
  86. {apte-0.3.0 → apte-0.3.2}/apte/reporting/verbosity.py +0 -0
  87. {apte-0.3.0 → apte-0.3.2}/apte/reporting/web.py +0 -0
  88. {apte-0.3.0 → apte-0.3.2}/apte/shell.py +0 -0
  89. {apte-0.3.0 → apte-0.3.2}/apte/tags/__init__.py +0 -0
  90. {apte-0.3.0 → apte-0.3.2}/apte/tags/plugin.py +0 -0
  91. {apte-0.3.0 → apte-0.3.2}/apte/utils.py +0 -0
  92. {apte-0.3.0 → apte-0.3.2}/apte.egg-info/SOURCES.txt +0 -0
  93. {apte-0.3.0 → apte-0.3.2}/apte.egg-info/dependency_links.txt +0 -0
  94. {apte-0.3.0 → apte-0.3.2}/apte.egg-info/entry_points.txt +0 -0
  95. {apte-0.3.0 → apte-0.3.2}/apte.egg-info/top_level.txt +0 -0
  96. {apte-0.3.0 → apte-0.3.2}/setup.cfg +0 -0
  97. {apte-0.3.0 → apte-0.3.2}/tests/test_assertions.py +0 -0
  98. {apte-0.3.0 → apte-0.3.2}/tests/test_console_print.py +0 -0
  99. {apte-0.3.0 → apte-0.3.2}/tests/test_normalizers.py +0 -0
  100. {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.0
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.15.0
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
+ ![Apte demo: tests and evals in one framework](assets/demo.gif)
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 git+https://github.com/renaudcepre/apte.git
156
+ uv add apte
157
157
 
158
158
  # With pip
159
- pip install git+https://github.com/renaudcepre/apte.git
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
+ ![Apte demo: tests and evals in one framework](assets/demo.gif)
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 git+https://github.com/renaudcepre/apte.git
125
+ uv add apte
126
126
 
127
127
  # With pip
128
- pip install git+https://github.com/renaudcepre/apte.git
128
+ pip install apte
129
129
  ```
130
130
 
131
131
  ## CLI
@@ -14,7 +14,7 @@ from apte.loader import LoadError, load_session
14
14
  from apte.plugin import PluginBase
15
15
  from apte.shell import CommandResult, Shell
16
16
 
17
- __version__ = "0.3.0"
17
+ __version__ = "0.3.2"
18
18
 
19
19
  __all__ = [
20
20
  "ApteError",
@@ -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
- return self._interrupt_handler.soft_stop_event.is_set() or (
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 are executed in a thread pool (blocking the event emission).
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(self, event: Event, handler: Callable[..., Any]) -> None:
88
- """Register a handler for an event."""
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(_RegisteredHandler(func=handler, name=name))
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 = asyncio.iscoroutinefunction(handler)
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
- else:
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
- if data is not None:
125
- await run_in_threadpool(
126
- self._run_sync_with_lock, lock, handler, data
127
- )
128
- else:
129
- await run_in_threadpool(
130
- self._run_sync_with_lock, lock, handler, None
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 self._handlers[Event.HANDLER_START]:
206
+ for handler_entry in listeners:
175
207
  try:
176
- if asyncio.iscoroutinefunction(handler_entry.func):
208
+ if handler_entry.is_async:
177
209
  await handler_entry.func(info)
178
210
  else:
179
- await run_in_threadpool(handler_entry.func, info)
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 self._handlers[Event.HANDLER_END]:
230
+ for handler_entry in listeners:
196
231
  try:
197
- if asyncio.iscoroutinefunction(handler_entry.func):
232
+ if handler_entry.is_async:
198
233
  await handler_entry.func(info)
199
234
  else:
200
- await run_in_threadpool(handler_entry.func, info)
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 asyncio.iscoroutinefunction(handler):
254
+ if handler_entry.is_async:
220
255
  result = await handler(data)
221
256
  else:
222
- result = await run_in_threadpool(handler, data)
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.0
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.15.0
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
+ ![Apte demo: tests and evals in one framework](assets/demo.gif)
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 git+https://github.com/renaudcepre/apte.git
156
+ uv add apte
157
157
 
158
158
  # With pip
159
- pip install git+https://github.com/renaudcepre/apte.git
159
+ pip install apte
160
160
  ```
161
161
 
162
162
  ## CLI
@@ -1,4 +1,4 @@
1
- typing-extensions>=4.15.0
1
+ typing-extensions>=4.16.0
2
2
 
3
3
  [rich]
4
4
  rich>=15.0.0
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apte"
7
- version = "0.3.0"
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.15.0",
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