shrinkray 25.12.26.2__py3-none-any.whl → 25.12.27.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
shrinkray/__main__.py CHANGED
@@ -75,12 +75,14 @@ async def run_shrink_ray(
75
75
  )
76
76
  @click.option(
77
77
  "--timeout",
78
- default=1,
78
+ default=None,
79
79
  type=click.FLOAT,
80
80
  help=(
81
- "Time out subprocesses after this many seconds. If set to <= 0 then "
82
- "no timeout will be used. Any commands that time out will be treated "
83
- "as failing the test"
81
+ "Time out subprocesses after this many seconds. If not specified, "
82
+ "runs the interestingness test once and sets timeout to 10x the "
83
+ "measured time (capped at 5 minutes). If set to <= 0 then no timeout "
84
+ "will be used. Any commands that time out will be treated as failing "
85
+ "the test"
84
86
  ),
85
87
  )
86
88
  @click.option(
@@ -212,7 +214,7 @@ def main(
212
214
  backup: str,
213
215
  filename: str,
214
216
  test: list[str],
215
- timeout: float,
217
+ timeout: float | None,
216
218
  in_place: bool,
217
219
  parallelism: int,
218
220
  seed: int,
@@ -225,7 +227,7 @@ def main(
225
227
  ui_type: UIType,
226
228
  theme: str,
227
229
  ) -> None:
228
- if timeout <= 0:
230
+ if timeout is not None and timeout <= 0:
229
231
  timeout = float("inf")
230
232
 
231
233
  if not os.access(test[0], os.X_OK):
@@ -271,20 +273,20 @@ def main(
271
273
  if not backup:
272
274
  backup = filename + os.extsep + "bak"
273
275
 
274
- state_kwargs: dict[str, Any] = dict(
275
- input_type=input_type,
276
- in_place=in_place,
277
- test=test,
278
- timeout=timeout,
279
- base=os.path.basename(filename),
280
- parallelism=parallelism,
281
- filename=filename,
282
- formatter=formatter,
283
- trivial_is_error=trivial_is_error,
284
- seed=seed,
285
- volume=volume,
286
- clang_delta_executable=clang_delta_executable,
287
- )
276
+ state_kwargs: dict[str, Any] = {
277
+ "input_type": input_type,
278
+ "in_place": in_place,
279
+ "test": test,
280
+ "timeout": timeout,
281
+ "base": os.path.basename(filename),
282
+ "parallelism": parallelism,
283
+ "filename": filename,
284
+ "formatter": formatter,
285
+ "trivial_is_error": trivial_is_error,
286
+ "seed": seed,
287
+ "volume": volume,
288
+ "clang_delta_executable": clang_delta_executable,
289
+ }
288
290
 
289
291
  state: ShrinkRayState[Any]
290
292
  ui: ShrinkRayUI[Any]
shrinkray/passes/sat.py CHANGED
@@ -165,14 +165,14 @@ async def renumber_variables(problem: ReductionProblem[SAT]) -> None:
165
165
  result: SAT = []
166
166
  for clause in sat:
167
167
  new_clause: Clause = sorted(
168
- set(
169
- [
168
+ {
169
+ (
170
170
  (renumbering[lit] if lit > 0 else -renumbering[-lit])
171
171
  if abs(lit) in renumbering
172
172
  else lit
173
- for lit in clause
174
- ]
175
- )
173
+ )
174
+ for lit in clause
175
+ }
176
176
  )
177
177
  if len(set(map(abs, new_clause))) == len(new_clause):
178
178
  result.append(new_clause)
@@ -290,7 +290,7 @@ class BooleanEquivalence(UnionFind[int]):
290
290
 
291
291
  def find(self, value: int) -> int:
292
292
  if not value:
293
- raise ValueError("Invalid variable %r" % (value,))
293
+ raise ValueError(f"Invalid variable {value!r}")
294
294
  return super().find(value)
295
295
 
296
296
  def merge(self, left: int, right: int) -> None:
shrinkray/reducer.py CHANGED
@@ -59,7 +59,9 @@ class Reducer[T](ABC):
59
59
  # Optional pass statistics tracking (implemented by ShrinkRay)
60
60
  pass_stats: "PassStatsTracker | None" = attrs.field(default=None, init=False)
61
61
  # Optional current pass tracking (implemented by ShrinkRay)
62
- current_reduction_pass: "ReductionPass[T] | None" = attrs.field(default=None, init=False)
62
+ current_reduction_pass: "ReductionPass[T] | None" = attrs.field(
63
+ default=None, init=False
64
+ )
63
65
 
64
66
  @contextmanager
65
67
  def backtrack(self, restart: T) -> Generator[None, None, None]:
@@ -192,7 +194,9 @@ class ShrinkRay(Reducer[bytes]):
192
194
  # Pass control: disabled passes and skip functionality
193
195
  disabled_passes: set[str] = attrs.Factory(set)
194
196
  _skip_requested: bool = attrs.field(default=False, init=False)
195
- _current_pass_scope: "trio.CancelScope | None" = attrs.field(default=None, init=False)
197
+ _current_pass_scope: "trio.CancelScope | None" = attrs.field(
198
+ default=None, init=False
199
+ )
196
200
  _passes_were_skipped: bool = attrs.field(default=False, init=False)
197
201
 
198
202
  def disable_pass(self, pass_name: str) -> None:
shrinkray/state.py CHANGED
@@ -36,13 +36,32 @@ class TimeoutExceededOnInitial(InvalidInitialExample):
36
36
  )
37
37
 
38
38
 
39
+ # Constants for dynamic timeout
40
+ DYNAMIC_TIMEOUT_CALIBRATION_TIMEOUT = 300.0 # 5 minutes for first call
41
+ DYNAMIC_TIMEOUT_MULTIPLIER = 10
42
+ DYNAMIC_TIMEOUT_MAX = 300.0 # 5 minutes maximum
43
+ DYNAMIC_TIMEOUT_MIN = 1.0 # 1 second minimum to prevent edge cases
44
+
45
+
46
+ def compute_dynamic_timeout(runtime: float) -> float:
47
+ """Compute dynamic timeout based on measured runtime.
48
+
49
+ The timeout is set to 10x the measured runtime, clamped between
50
+ DYNAMIC_TIMEOUT_MIN and DYNAMIC_TIMEOUT_MAX.
51
+ """
52
+ return max(
53
+ DYNAMIC_TIMEOUT_MIN,
54
+ min(runtime * DYNAMIC_TIMEOUT_MULTIPLIER, DYNAMIC_TIMEOUT_MAX),
55
+ )
56
+
57
+
39
58
  @define(slots=False)
40
59
  class ShrinkRayState[TestCase](ABC):
41
60
  input_type: Any # InputType from __main__
42
61
  in_place: bool
43
62
  test: list[str]
44
63
  filename: str
45
- timeout: float
64
+ timeout: float | None
46
65
  base: str
47
66
  parallelism: int
48
67
  initial: TestCase
@@ -105,12 +124,12 @@ class ShrinkRayState[TestCase](ABC):
105
124
  else:
106
125
  command = self.test
107
126
 
108
- kwargs: dict[str, Any] = dict(
109
- universal_newlines=False,
110
- preexec_fn=os.setsid,
111
- cwd=cwd,
112
- check=False,
113
- )
127
+ kwargs: dict[str, Any] = {
128
+ "universal_newlines": False,
129
+ "preexec_fn": os.setsid,
130
+ "cwd": cwd,
131
+ "check": False,
132
+ }
114
133
  if self.input_type.enabled(self._InputType.stdin) and not os.path.isdir(
115
134
  working
116
135
  ):
@@ -127,7 +146,8 @@ class ShrinkRayState[TestCase](ABC):
127
146
  completed = await trio.run_process(command, **kwargs)
128
147
  runtime = time.time() - start_time
129
148
 
130
- if runtime >= self.timeout and self.first_call:
149
+ # Check for timeout violation (only when timeout is explicitly set)
150
+ if self.timeout is not None and runtime >= self.timeout and self.first_call:
131
151
  self.initial_exit_code = completed.returncode
132
152
  self.first_call = False
133
153
  raise TimeoutExceededOnInitial(
@@ -137,6 +157,9 @@ class ShrinkRayState[TestCase](ABC):
137
157
 
138
158
  if self.first_call:
139
159
  self.initial_exit_code = completed.returncode
160
+ # Set dynamic timeout if not explicitly specified
161
+ if self.timeout is None:
162
+ self.timeout = compute_dynamic_timeout(runtime)
140
163
  self.first_call = False
141
164
 
142
165
  # Store captured output
@@ -168,9 +191,19 @@ class ShrinkRayState[TestCase](ABC):
168
191
  sp = await nursery.start(call_with_kwargs)
169
192
 
170
193
  try:
171
- with trio.move_on_after(
172
- self.timeout * 10 if self.first_call else self.timeout
173
- ):
194
+ # Determine effective timeout for this call
195
+ if self.first_call:
196
+ # For first call: use calibration timeout if dynamic, otherwise 10x explicit timeout
197
+ if self.timeout is None:
198
+ effective_timeout = DYNAMIC_TIMEOUT_CALIBRATION_TIMEOUT
199
+ else:
200
+ effective_timeout = self.timeout * 10
201
+ else:
202
+ # For subsequent calls, timeout must be set (either explicit or computed)
203
+ assert self.timeout is not None
204
+ effective_timeout = self.timeout
205
+
206
+ with trio.move_on_after(effective_timeout):
174
207
  await sp.wait()
175
208
 
176
209
  runtime = time.time() - start_time
@@ -179,7 +212,12 @@ class ShrinkRayState[TestCase](ABC):
179
212
  # Process didn't terminate before timeout - kill it
180
213
  await self._interrupt_wait_and_kill(sp)
181
214
 
182
- if runtime >= self.timeout and self.first_call:
215
+ # Check for timeout violation (only when timeout is explicitly set)
216
+ if (
217
+ self.timeout is not None
218
+ and runtime >= self.timeout
219
+ and self.first_call
220
+ ):
183
221
  raise TimeoutExceededOnInitial(
184
222
  timeout=self.timeout,
185
223
  runtime=runtime,
@@ -187,6 +225,10 @@ class ShrinkRayState[TestCase](ABC):
187
225
  finally:
188
226
  if self.first_call:
189
227
  self.initial_exit_code = sp.returncode
228
+ # Set dynamic timeout if not explicitly specified
229
+ if self.timeout is None:
230
+ runtime = time.time() - start_time
231
+ self.timeout = compute_dynamic_timeout(runtime)
190
232
  self.first_call = False
191
233
 
192
234
  result: int | None = sp.returncode
@@ -562,10 +604,10 @@ class ShrinkRayDirectoryState(ShrinkRayState[dict[str, bytes]]):
562
604
  sorted((k, shortlex(v)) for k, v in test_case.items()),
563
605
  )
564
606
 
565
- return dict(
566
- sort_key=dict_sort_key,
567
- size=dict_size,
568
- )
607
+ return {
608
+ "sort_key": dict_sort_key,
609
+ "size": dict_size,
610
+ }
569
611
 
570
612
  def new_reducer(
571
613
  self, problem: ReductionProblem[dict[str, bytes]]
@@ -4,7 +4,7 @@ import asyncio
4
4
  import sys
5
5
  import traceback
6
6
  import uuid
7
- from collections.abc import AsyncIterator
7
+ from collections.abc import AsyncGenerator
8
8
  from typing import Any
9
9
 
10
10
  from shrinkray.subprocess.protocol import (
@@ -127,7 +127,7 @@ class SubprocessClient:
127
127
  file_path: str,
128
128
  test: list[str],
129
129
  parallelism: int | None = None,
130
- timeout: float = 1.0,
130
+ timeout: float | None = None,
131
131
  seed: int = 0,
132
132
  input_type: str = "all",
133
133
  in_place: bool = False,
@@ -138,10 +138,9 @@ class SubprocessClient:
138
138
  trivial_is_error: bool = True,
139
139
  ) -> Response:
140
140
  """Start the reduction process."""
141
- params = {
141
+ params: dict[str, Any] = {
142
142
  "file_path": file_path,
143
143
  "test": test,
144
- "timeout": timeout,
145
144
  "seed": seed,
146
145
  "input_type": input_type,
147
146
  "in_place": in_place,
@@ -153,6 +152,8 @@ class SubprocessClient:
153
152
  }
154
153
  if parallelism is not None:
155
154
  params["parallelism"] = parallelism
155
+ if timeout is not None:
156
+ params["timeout"] = timeout
156
157
  return await self.send_command("start", params)
157
158
 
158
159
  async def get_status(self) -> Response:
@@ -200,7 +201,7 @@ class SubprocessClient:
200
201
  traceback.print_exc()
201
202
  return Response(id="", error="Failed to skip pass")
202
203
 
203
- async def get_progress_updates(self) -> AsyncIterator[ProgressUpdate]:
204
+ async def get_progress_updates(self) -> AsyncGenerator[ProgressUpdate, None]:
204
205
  """Yield progress updates as they arrive."""
205
206
  while not self._completed:
206
207
  try:
@@ -4,6 +4,7 @@ import os
4
4
  import sys
5
5
  import time
6
6
  import traceback
7
+ from contextlib import aclosing
7
8
  from typing import Any, Protocol
8
9
 
9
10
  import trio
@@ -25,6 +26,7 @@ class InputStream(Protocol):
25
26
 
26
27
  def __aiter__(self) -> "InputStream": ...
27
28
  async def __anext__(self) -> bytes | bytearray: ...
29
+ async def aclose(self) -> None: ...
28
30
 
29
31
 
30
32
  class OutputStream(Protocol):
@@ -88,12 +90,13 @@ class ReducerWorker:
88
90
  stream = trio.lowlevel.FdStream(os.dup(sys.stdin.fileno()))
89
91
 
90
92
  buffer = b""
91
- async for chunk in stream:
92
- buffer += chunk
93
- while b"\n" in buffer:
94
- line, buffer = buffer.split(b"\n", 1)
95
- if line:
96
- await self.handle_line(line.decode("utf-8"))
93
+ async with aclosing(stream) as aiter:
94
+ async for chunk in aiter:
95
+ buffer += chunk
96
+ while b"\n" in buffer:
97
+ line, buffer = buffer.split(b"\n", 1)
98
+ if line:
99
+ await self.handle_line(line.decode("utf-8"))
97
100
 
98
101
  async def handle_line(self, line: str) -> None:
99
102
  """Handle a single command line."""
@@ -124,7 +127,9 @@ class ReducerWorker:
124
127
  case "skip_pass":
125
128
  return self._handle_skip_pass(request.id)
126
129
  case _:
127
- return Response(id=request.id, error=f"Unknown command: {request.command}")
130
+ return Response(
131
+ id=request.id, error=f"Unknown command: {request.command}"
132
+ )
128
133
 
129
134
  async def _handle_start(self, request_id: str, params: dict) -> Response:
130
135
  """Start the reduction process."""
@@ -164,7 +169,7 @@ class ReducerWorker:
164
169
  filename = params["file_path"]
165
170
  test = params["test"]
166
171
  parallelism = params.get("parallelism", os.cpu_count() or 1)
167
- timeout = params.get("timeout", 1.0)
172
+ timeout = params.get("timeout") # None means dynamic timeout
168
173
  seed = params.get("seed", 0)
169
174
  input_type = InputType[params.get("input_type", "all")]
170
175
  in_place = params.get("in_place", False)
@@ -181,20 +186,20 @@ class ReducerWorker:
181
186
  if clang_delta_path:
182
187
  clang_delta_executable = ClangDelta(clang_delta_path)
183
188
 
184
- state_kwargs: dict[str, Any] = dict(
185
- input_type=input_type,
186
- in_place=in_place,
187
- test=test,
188
- timeout=timeout,
189
- base=os.path.basename(filename),
190
- parallelism=parallelism,
191
- filename=filename,
192
- formatter=formatter,
193
- trivial_is_error=trivial_is_error,
194
- seed=seed,
195
- volume=volume,
196
- clang_delta_executable=clang_delta_executable,
197
- )
189
+ state_kwargs: dict[str, Any] = {
190
+ "input_type": input_type,
191
+ "in_place": in_place,
192
+ "test": test,
193
+ "timeout": timeout,
194
+ "base": os.path.basename(filename),
195
+ "parallelism": parallelism,
196
+ "filename": filename,
197
+ "formatter": formatter,
198
+ "trivial_is_error": trivial_is_error,
199
+ "seed": seed,
200
+ "volume": volume,
201
+ "clang_delta_executable": clang_delta_executable,
202
+ }
198
203
 
199
204
  if os.path.isdir(filename):
200
205
  files = [os.path.join(d, f) for d, _, fs in os.walk(filename) for f in fs]
@@ -263,7 +268,9 @@ class ReducerWorker:
263
268
 
264
269
  if self.reducer is not None and hasattr(self.reducer, "disable_pass"):
265
270
  self.reducer.disable_pass(pass_name)
266
- return Response(id=request_id, result={"status": "disabled", "pass_name": pass_name})
271
+ return Response(
272
+ id=request_id, result={"status": "disabled", "pass_name": pass_name}
273
+ )
267
274
  return Response(id=request_id, error="Reducer does not support pass control")
268
275
 
269
276
  def _handle_enable_pass(self, request_id: str, params: dict) -> Response:
@@ -281,7 +288,9 @@ class ReducerWorker:
281
288
 
282
289
  if self.reducer is not None and hasattr(self.reducer, "enable_pass"):
283
290
  self.reducer.enable_pass(pass_name)
284
- return Response(id=request_id, result={"status": "enabled", "pass_name": pass_name})
291
+ return Response(
292
+ id=request_id, result={"status": "enabled", "pass_name": pass_name}
293
+ )
285
294
  return Response(id=request_id, error="Reducer does not support pass control")
286
295
 
287
296
  def _handle_skip_pass(self, request_id: str) -> Response:
shrinkray/tui.py CHANGED
@@ -1,7 +1,9 @@
1
1
  """Textual-based TUI for Shrink Ray."""
2
2
 
3
3
  import os
4
- from collections.abc import AsyncIterator
4
+ import traceback
5
+ from collections.abc import AsyncGenerator
6
+ from contextlib import aclosing
5
7
  from datetime import timedelta
6
8
  from typing import Literal, Protocol
7
9
 
@@ -96,7 +98,7 @@ class ReductionClientProtocol(Protocol):
96
98
  file_path: str,
97
99
  test: list[str],
98
100
  parallelism: int | None = None,
99
- timeout: float = 1.0,
101
+ timeout: float | None = None,
100
102
  seed: int = 0,
101
103
  input_type: str = "all",
102
104
  in_place: bool = False,
@@ -114,7 +116,7 @@ class ReductionClientProtocol(Protocol):
114
116
 
115
117
  @property
116
118
  def error_message(self) -> str | None: ...
117
- def get_progress_updates(self) -> AsyncIterator[ProgressUpdate]: ...
119
+ def get_progress_updates(self) -> AsyncGenerator[ProgressUpdate, None]: ...
118
120
  @property
119
121
  def is_completed(self) -> bool: ...
120
122
 
@@ -485,7 +487,9 @@ class PassStatsScreen(ModalScreen[None]):
485
487
  reductions = str(ps.successful_reductions)
486
488
  success = f"{ps.success_rate:.1f}%"
487
489
 
488
- table.add_row(checkbox, name, runs, bytes_del, tests, reductions, success)
490
+ table.add_row(
491
+ checkbox, name, runs, bytes_del, tests, reductions, success
492
+ )
489
493
 
490
494
  # Restore cursor and scroll position after rebuilding
491
495
  # Only restore if the saved position is still valid
@@ -510,7 +514,9 @@ class PassStatsScreen(ModalScreen[None]):
510
514
  # Update footer with disabled count
511
515
  disabled_count = len(self.disabled_passes)
512
516
  if disabled_count > 0:
513
- footer_text = f"Showing {len(self.pass_stats)} passes ({disabled_count} disabled)"
517
+ footer_text = (
518
+ f"Showing {len(self.pass_stats)} passes ({disabled_count} disabled)"
519
+ )
514
520
  else:
515
521
  footer_text = f"Showing {len(self.pass_stats)} passes in run order"
516
522
  footer = self.query_one("#stats-footer", Static)
@@ -610,7 +616,7 @@ class ShrinkRayApp(App[None]):
610
616
  file_path: str,
611
617
  test: list[str],
612
618
  parallelism: int | None = None,
613
- timeout: float = 1.0,
619
+ timeout: float | None = None,
614
620
  seed: int = 0,
615
621
  input_type: str = "all",
616
622
  in_place: bool = False,
@@ -651,6 +657,7 @@ class ShrinkRayApp(App[None]):
651
657
  yield Label(
652
658
  "Shrink Ray - [h] help, [p] passes, [c] skip pass, [q] quit",
653
659
  id="status-label",
660
+ markup=False,
654
661
  )
655
662
  with Vertical(id="stats-container"):
656
663
  yield StatsDisplay(id="stats-display")
@@ -713,18 +720,21 @@ class ShrinkRayApp(App[None]):
713
720
  stats_display = self.query_one("#stats-display", StatsDisplay)
714
721
  content_preview = self.query_one("#content-preview", ContentPreview)
715
722
 
716
- async for update in self._client.get_progress_updates():
717
- stats_display.update_stats(update)
718
- content_preview.update_content(update.content_preview, update.hex_mode)
719
- self._latest_pass_stats = update.pass_stats
720
- self._current_pass_name = update.current_pass_name
721
- self._disabled_passes = update.disabled_passes
723
+ async with aclosing(self._client.get_progress_updates()) as updates:
724
+ async for update in updates:
725
+ stats_display.update_stats(update)
726
+ content_preview.update_content(
727
+ update.content_preview, update.hex_mode
728
+ )
729
+ self._latest_pass_stats = update.pass_stats
730
+ self._current_pass_name = update.current_pass_name
731
+ self._disabled_passes = update.disabled_passes
722
732
 
723
- # Check if all passes are disabled
724
- self._check_all_passes_disabled()
733
+ # Check if all passes are disabled
734
+ self._check_all_passes_disabled()
725
735
 
726
- if self._client.is_completed:
727
- break
736
+ if self._client.is_completed:
737
+ break
728
738
 
729
739
  self._completed = True
730
740
 
@@ -739,6 +749,7 @@ class ShrinkRayApp(App[None]):
739
749
  self.update_status("Reduction completed! Press 'q' to exit.")
740
750
 
741
751
  except Exception as e:
752
+ traceback.print_exc()
742
753
  self.exit(return_code=1, message=f"Error: {e}")
743
754
  finally:
744
755
  if self._owns_client and self._client:
@@ -750,7 +761,7 @@ class ShrinkRayApp(App[None]):
750
761
  all_pass_names = {ps.pass_name for ps in self._latest_pass_stats}
751
762
  if all_pass_names and all_pass_names <= set(self._disabled_passes):
752
763
  self.update_status(
753
- "Reduction paused (all passes disabled) - " "[p] to re-enable passes"
764
+ "Reduction paused (all passes disabled) - [p] to re-enable passes"
754
765
  )
755
766
 
756
767
  def update_status(self, message: str) -> None:
@@ -797,7 +808,7 @@ async def _validate_initial_example(
797
808
  file_path: str,
798
809
  test: list[str],
799
810
  parallelism: int | None,
800
- timeout: float,
811
+ timeout: float | None,
801
812
  seed: int,
802
813
  input_type: str,
803
814
  in_place: bool,
@@ -845,7 +856,7 @@ def run_textual_ui(
845
856
  file_path: str,
846
857
  test: list[str],
847
858
  parallelism: int | None = None,
848
- timeout: float = 1.0,
859
+ timeout: float | None = None,
849
860
  seed: int = 0,
850
861
  input_type: str = "all",
851
862
  in_place: bool = False,
shrinkray/work.py CHANGED
@@ -13,7 +13,7 @@ Key concepts:
13
13
 
14
14
  import heapq
15
15
  from collections.abc import Awaitable, Callable, Sequence
16
- from contextlib import asynccontextmanager
16
+ from contextlib import aclosing, asynccontextmanager
17
17
  from enum import IntEnum
18
18
  from itertools import islice
19
19
  from random import Random
@@ -100,8 +100,9 @@ class WorkContext:
100
100
  async with parallel_map(
101
101
  values, f, parallelism=min(self.parallelism, n)
102
102
  ) as result:
103
- async for v in result:
104
- await send.send(v)
103
+ async with aclosing(result) as aiter:
104
+ async for v in aiter:
105
+ await send.send(v)
105
106
 
106
107
  n *= 2
107
108
  else:
@@ -122,9 +123,10 @@ class WorkContext:
122
123
  @nursery.start_soon
123
124
  async def _():
124
125
  async with self.map(ls, apply) as results:
125
- async for x, v in results:
126
- if v:
127
- await send.send(x)
126
+ async with aclosing(results) as aiter:
127
+ async for x, v in aiter:
128
+ if v:
129
+ await send.send(x)
128
130
  send.close()
129
131
 
130
132
  yield receive
@@ -139,8 +141,9 @@ class WorkContext:
139
141
  Will run in parallel if parallelism is enabled.
140
142
  """
141
143
  async with self.filter(ls, f) as filtered:
142
- async for x in filtered:
143
- return x
144
+ async with aclosing(filtered) as aiter:
145
+ async for x in aiter:
146
+ return x
144
147
  raise NotFound()
145
148
 
146
149
  async def find_large_integer(self, f: Callable[[int], Awaitable[bool]]) -> int:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shrinkray
3
- Version: 25.12.26.2
3
+ Version: 25.12.27.1
4
4
  Summary: Shrink Ray
5
5
  Author-email: "David R. MacIver" <david@drmaciver.com>
6
6
  License: MIT
@@ -28,7 +28,8 @@ Requires-Dist: hypothesmith>=0.3.1; extra == "dev"
28
28
  Requires-Dist: pytest>=8.0.0; extra == "dev"
29
29
  Requires-Dist: pytest-trio>=0.8.0; extra == "dev"
30
30
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
31
- Requires-Dist: pytest-textual-snapshot>=1.0.0; extra == "dev"
31
+ Requires-Dist: syrupy>=5.0.0; extra == "dev"
32
+ Requires-Dist: jinja2>=3.0.0; extra == "dev"
32
33
  Requires-Dist: coverage[toml]>=7.4.0; extra == "dev"
33
34
  Requires-Dist: pygments>=2.17.0; extra == "dev"
34
35
  Requires-Dist: basedpyright>=1.1.0; extra == "dev"
@@ -1,16 +1,16 @@
1
1
  shrinkray/__init__.py,sha256=b5MvcvhsEGYya3GRXNbCJAlAL5IZHSsETLK_vtfmXRY,18
2
- shrinkray/__main__.py,sha256=sRYLrG-7FMa-y067JyYmLZMOkO2FJ2V-BenxqtBwQj0,10887
2
+ shrinkray/__main__.py,sha256=K3_s96Tyoi7SxNOyoZXkfiEoSxVBL__TJ3o2Cefadmg,11093
3
3
  shrinkray/cli.py,sha256=1-qjaIchyCDd-YCdGWtK7q9j9qr6uX6AqtwW8m5QCQg,1697
4
4
  shrinkray/display.py,sha256=WYN05uqmUVpZhwi2pxr1U-wLHWZ9KiL0RUlTCBJ1N3E,2430
5
5
  shrinkray/formatting.py,sha256=tXCGnhJn-WJGpHMaLHRCAXK8aKJBbrOdiW9QGERrQEk,3121
6
6
  shrinkray/problem.py,sha256=Kp7QN10E4tzjdpoqJve8_RT26VpywzQwY0gX2VkBGCo,17277
7
7
  shrinkray/process.py,sha256=-eP8h5X0ESbkcTic8FFEzkd4-vwaZ0YI5tLxUR25L8U,1599
8
8
  shrinkray/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- shrinkray/reducer.py,sha256=66Q5BjTLKamO2M04i2CSrbThp7PyGTRu63_ueQnjc7g,19849
10
- shrinkray/state.py,sha256=_-gyAkUm0vEdF1U0Fz_Deykj-kY2u3nGn3X6BfH3viA,22371
11
- shrinkray/tui.py,sha256=HtvqimSr1r7IX_fukRsCsVxyhEdyj2W-HLqjVATt1eM,31235
9
+ shrinkray/reducer.py,sha256=xhLo_GF7qrIVoiLHed6Wt4nxjdE-9jj_7K9F76un89o,19877
10
+ shrinkray/state.py,sha256=HQ7VivCXWiP2L53gB0lh6dHcdru6waWZkbPg6rYO7z4,24290
11
+ shrinkray/tui.py,sha256=3RskLo6JvKdUQIHi40R5ka-F_1GkBXyA_d_SkYbLlCw,31601
12
12
  shrinkray/ui.py,sha256=xuDUwU-MM3AetvwUB7bfzav0P_drUsBrKFPhON_Nr-k,2251
13
- shrinkray/work.py,sha256=DXeqJTB_G8r7e8vrsMW2J56CJ-nhgymKBH55DT8SXs8,7901
13
+ shrinkray/work.py,sha256=GEZ14Kk3bvwUxAnACvY-wom2lVWaGrELMNxrDjv03dk,8110
14
14
  shrinkray/passes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  shrinkray/passes/bytes.py,sha256=pX2kBeH38SM4j55f8bNF_2PVBCDo6XdUj73EvOFH9f8,23904
16
16
  shrinkray/passes/clangdelta.py,sha256=t9EQ_kc159HRs48JwB5JvlJCsiCscrZgf2nhHCZRZX0,8419
@@ -19,15 +19,15 @@ shrinkray/passes/genericlanguages.py,sha256=qbTuJgUieHqWtf7cly2tm0qdbrVXzVoWcAnF
19
19
  shrinkray/passes/json.py,sha256=AcmroHgb38Aa3aApwcuYNzmyya_vnCi2RFSVqomiDg8,2586
20
20
  shrinkray/passes/patching.py,sha256=1uOTir3IbywKmsg6IIhSnxHFovZTdUCS-8PSwzgza00,8936
21
21
  shrinkray/passes/python.py,sha256=3WN1lZTf5oVL8FCTGomhrCuE04wIX9ocKcmFV86NMZA,6875
22
- shrinkray/passes/sat.py,sha256=2FGMM4rh4AX1BVEbry082C4aLCOEOXlfr3exHbxYgSQ,19514
22
+ shrinkray/passes/sat.py,sha256=5Zv4IgGfg3SYplMAAaPLkbBNh4fuVpci4F9GdVacayA,19504
23
23
  shrinkray/passes/sequences.py,sha256=jCK1fWBxCz79u7JWSps9wf7Yru7W_FAsJwdgg--CLxU,3040
24
24
  shrinkray/subprocess/__init__.py,sha256=FyV2y05uwQ1RTZGwREI0aAVaLX1jiwRcWsdsksFmdbM,451
25
- shrinkray/subprocess/client.py,sha256=xSFqm5UyQT0WJ5aBVVkuiWDsHjZYv7RqjgrjjyX0rK0,9269
25
+ shrinkray/subprocess/client.py,sha256=erqnPglPO0YNdwEKlmhB3yDo6Mfc00Lxh4T85lZhsDo,9341
26
26
  shrinkray/subprocess/protocol.py,sha256=LuHl0IkKpDzYhAGZz_EiTHNqDNq_v1ozg5aUSl7UzE4,6203
27
- shrinkray/subprocess/worker.py,sha256=MbubmnuXNFxD_SRKdiFDkGhdkCEgRxdn0tPN0HoJpyk,18998
28
- shrinkray-25.12.26.2.dist-info/licenses/LICENSE,sha256=iMKX79AuokJfIZUnGUARdUp30vVAoIPOJ7ek8TY63kk,1072
29
- shrinkray-25.12.26.2.dist-info/METADATA,sha256=3EX6HRC0LFouM2oCEKcOCjB1K1yfN1O0LrvT7LlWHh8,9693
30
- shrinkray-25.12.26.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- shrinkray-25.12.26.2.dist-info/entry_points.txt,sha256=wIZvnGyOdVeaLTiv2klnSyTe-EKkkwn4SwHh9bmJ7qk,104
32
- shrinkray-25.12.26.2.dist-info/top_level.txt,sha256=fLif8-rFoFOnf5h8-vs3ECkKNWQopTQh3xvl1s7pchQ,10
33
- shrinkray-25.12.26.2.dist-info/RECORD,,
27
+ shrinkray/subprocess/worker.py,sha256=ke-9DYFH117EpJEntkucTrn7ep7pygzmV-VXkRe1o-E,19294
28
+ shrinkray-25.12.27.1.dist-info/licenses/LICENSE,sha256=iMKX79AuokJfIZUnGUARdUp30vVAoIPOJ7ek8TY63kk,1072
29
+ shrinkray-25.12.27.1.dist-info/METADATA,sha256=bB-_7Y4Gk9a-QiXEFKy_iTDy3nWM72YOYXhq0W_PJ7o,9721
30
+ shrinkray-25.12.27.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ shrinkray-25.12.27.1.dist-info/entry_points.txt,sha256=wIZvnGyOdVeaLTiv2klnSyTe-EKkkwn4SwHh9bmJ7qk,104
32
+ shrinkray-25.12.27.1.dist-info/top_level.txt,sha256=fLif8-rFoFOnf5h8-vs3ECkKNWQopTQh3xvl1s7pchQ,10
33
+ shrinkray-25.12.27.1.dist-info/RECORD,,