shrinkray 25.12.26.2__tar.gz → 25.12.27.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.
Files changed (66) hide show
  1. {shrinkray-25.12.26.2/src/shrinkray.egg-info → shrinkray-25.12.27.0}/PKG-INFO +1 -1
  2. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/pyproject.toml +1 -3
  3. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/__main__.py +14 -14
  4. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/sat.py +6 -6
  5. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/reducer.py +6 -2
  6. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/state.py +10 -10
  7. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/client.py +2 -2
  8. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/worker.py +32 -23
  9. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/tui.py +25 -15
  10. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/work.py +11 -8
  11. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0/src/shrinkray.egg-info}/PKG-INFO +1 -1
  12. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_clang_delta.py +18 -18
  13. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_cli.py +24 -16
  14. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_definitions.py +1 -0
  15. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_dimacs_cnf.py +8 -4
  16. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_formatting.py +6 -4
  17. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_generic_language.py +0 -2
  18. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_main.py +25 -37
  19. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_problem.py +9 -13
  20. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_reducer.py +11 -38
  21. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_reduction_passes.py +10 -21
  22. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_state.py +8 -10
  23. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_client.py +15 -28
  24. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_integration.py +3 -12
  25. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_worker.py +24 -44
  26. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_tui.py +60 -117
  27. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_tui_snapshots.py +6 -2
  28. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_work.py +17 -10
  29. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/LICENSE +0 -0
  30. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/README.md +0 -0
  31. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/setup.cfg +0 -0
  32. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/__init__.py +0 -0
  33. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/cli.py +0 -0
  34. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/display.py +0 -0
  35. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/formatting.py +0 -0
  36. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/__init__.py +0 -0
  37. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/bytes.py +0 -0
  38. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/clangdelta.py +0 -0
  39. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/definitions.py +0 -0
  40. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/genericlanguages.py +0 -0
  41. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/json.py +0 -0
  42. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/patching.py +0 -0
  43. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/python.py +0 -0
  44. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/sequences.py +0 -0
  45. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/problem.py +0 -0
  46. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/process.py +0 -0
  47. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/py.typed +0 -0
  48. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/__init__.py +0 -0
  49. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/protocol.py +0 -0
  50. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/ui.py +0 -0
  51. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/SOURCES.txt +0 -0
  52. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/dependency_links.txt +0 -0
  53. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/entry_points.txt +0 -0
  54. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/requires.txt +0 -0
  55. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/top_level.txt +0 -0
  56. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_byte_reduction_passes.py +0 -0
  57. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_display.py +0 -0
  58. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_generic_shrinking_properties.py +0 -0
  59. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_json_passes.py +0 -0
  60. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_misc_reduction_performance.py +0 -0
  61. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_patching.py +0 -0
  62. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_process.py +0 -0
  63. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_python_reducers.py +0 -0
  64. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_sat.py +0 -0
  65. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_protocol.py +0 -0
  66. {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_ui.py +0 -0
@@ -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.0
4
4
  Summary: Shrink Ray
5
5
  Author-email: "David R. MacIver" <david@drmaciver.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shrinkray"
3
- version = "25.12.26.2"
3
+ version = "25.12.27.0"
4
4
  description = "Shrink Ray"
5
5
  authors = [
6
6
  {name = "David R. MacIver", email = "david@drmaciver.com"}
@@ -77,10 +77,8 @@ ignore = [
77
77
  "B007", # unused loop variable
78
78
  "B023", # function definition in loop
79
79
  "B904", # raise without from - intentional in some exception handling
80
- "C408", # unnecessary dict() call - sometimes more readable
81
80
  "UP031", # use format specifiers - % format is fine
82
81
  "B018", # useless expression - sometimes intentional for side effects
83
- "C403", # unnecessary list comprehension - sometimes more readable
84
82
  "UP022", # prefer capture_output - explicit is fine
85
83
  ]
86
84
 
@@ -271,20 +271,20 @@ def main(
271
271
  if not backup:
272
272
  backup = filename + os.extsep + "bak"
273
273
 
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
- )
274
+ state_kwargs: dict[str, Any] = {
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
+ }
288
288
 
289
289
  state: ShrinkRayState[Any]
290
290
  ui: ShrinkRayUI[Any]
@@ -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:
@@ -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:
@@ -105,12 +105,12 @@ class ShrinkRayState[TestCase](ABC):
105
105
  else:
106
106
  command = self.test
107
107
 
108
- kwargs: dict[str, Any] = dict(
109
- universal_newlines=False,
110
- preexec_fn=os.setsid,
111
- cwd=cwd,
112
- check=False,
113
- )
108
+ kwargs: dict[str, Any] = {
109
+ "universal_newlines": False,
110
+ "preexec_fn": os.setsid,
111
+ "cwd": cwd,
112
+ "check": False,
113
+ }
114
114
  if self.input_type.enabled(self._InputType.stdin) and not os.path.isdir(
115
115
  working
116
116
  ):
@@ -562,10 +562,10 @@ class ShrinkRayDirectoryState(ShrinkRayState[dict[str, bytes]]):
562
562
  sorted((k, shortlex(v)) for k, v in test_case.items()),
563
563
  )
564
564
 
565
- return dict(
566
- sort_key=dict_sort_key,
567
- size=dict_size,
568
- )
565
+ return {
566
+ "sort_key": dict_sort_key,
567
+ "size": dict_size,
568
+ }
569
569
 
570
570
  def new_reducer(
571
571
  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 (
@@ -200,7 +200,7 @@ class SubprocessClient:
200
200
  traceback.print_exc()
201
201
  return Response(id="", error="Failed to skip pass")
202
202
 
203
- async def get_progress_updates(self) -> AsyncIterator[ProgressUpdate]:
203
+ async def get_progress_updates(self) -> AsyncGenerator[ProgressUpdate, None]:
204
204
  """Yield progress updates as they arrive."""
205
205
  while not self._completed:
206
206
  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."""
@@ -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:
@@ -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
 
@@ -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)
@@ -713,18 +719,21 @@ class ShrinkRayApp(App[None]):
713
719
  stats_display = self.query_one("#stats-display", StatsDisplay)
714
720
  content_preview = self.query_one("#content-preview", ContentPreview)
715
721
 
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
722
+ async with aclosing(self._client.get_progress_updates()) as updates:
723
+ async for update in updates:
724
+ stats_display.update_stats(update)
725
+ content_preview.update_content(
726
+ update.content_preview, update.hex_mode
727
+ )
728
+ self._latest_pass_stats = update.pass_stats
729
+ self._current_pass_name = update.current_pass_name
730
+ self._disabled_passes = update.disabled_passes
722
731
 
723
- # Check if all passes are disabled
724
- self._check_all_passes_disabled()
732
+ # Check if all passes are disabled
733
+ self._check_all_passes_disabled()
725
734
 
726
- if self._client.is_completed:
727
- break
735
+ if self._client.is_completed:
736
+ break
728
737
 
729
738
  self._completed = True
730
739
 
@@ -739,6 +748,7 @@ class ShrinkRayApp(App[None]):
739
748
  self.update_status("Reduction completed! Press 'q' to exit.")
740
749
 
741
750
  except Exception as e:
751
+ traceback.print_exc()
742
752
  self.exit(return_code=1, message=f"Error: {e}")
743
753
  finally:
744
754
  if self._owns_client and self._client:
@@ -750,7 +760,7 @@ class ShrinkRayApp(App[None]):
750
760
  all_pass_names = {ps.pass_name for ps in self._latest_pass_stats}
751
761
  if all_pass_names and all_pass_names <= set(self._disabled_passes):
752
762
  self.update_status(
753
- "Reduction paused (all passes disabled) - " "[p] to re-enable passes"
763
+ "Reduction paused (all passes disabled) - [p] to re-enable passes"
754
764
  )
755
765
 
756
766
  def update_status(self, message: str) -> None:
@@ -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.0
4
4
  Summary: Shrink Ray
5
5
  Author-email: "David R. MacIver" <david@drmaciver.com>
6
6
  License: MIT
@@ -166,9 +166,8 @@ async def test_apply_transformation_raises_clang_delta_error():
166
166
  error.stdout = b"Some error"
167
167
  error.stderr = b""
168
168
 
169
- with patch("trio.run_process", side_effect=error):
170
- with pytest.raises(ClangDeltaError):
171
- await cd.apply_transformation("rename-var", 1, source)
169
+ with patch("trio.run_process", side_effect=error), pytest.raises(ClangDeltaError):
170
+ await cd.apply_transformation("rename-var", 1, source)
172
171
 
173
172
 
174
173
  async def test_pump_handles_assertion_failure():
@@ -238,14 +237,16 @@ def test_clang_delta_works_when_not_found():
238
237
  def test_clang_delta_works_when_execution_fails():
239
238
  """Test clang_delta_works returns False when clang_delta exists but fails to run."""
240
239
  clang_delta_works.cache_clear()
241
- with patch(
242
- "shrinkray.passes.clangdelta.find_clang_delta", return_value="/usr/bin/fake"
243
- ):
244
- with patch(
240
+ with (
241
+ patch(
242
+ "shrinkray.passes.clangdelta.find_clang_delta", return_value="/usr/bin/fake"
243
+ ),
244
+ patch(
245
245
  "shrinkray.passes.clangdelta.subprocess.run",
246
246
  side_effect=OSError("Library not found"),
247
- ):
248
- assert clang_delta_works() is False
247
+ ),
248
+ ):
249
+ assert clang_delta_works() is False
249
250
 
250
251
 
251
252
  def test_clang_delta_works_when_returns_nonzero():
@@ -254,13 +255,13 @@ def test_clang_delta_works_when_returns_nonzero():
254
255
  mock_result = MagicMock()
255
256
  mock_result.returncode = 1
256
257
 
257
- with patch(
258
- "shrinkray.passes.clangdelta.find_clang_delta", return_value="/usr/bin/fake"
258
+ with (
259
+ patch(
260
+ "shrinkray.passes.clangdelta.find_clang_delta", return_value="/usr/bin/fake"
261
+ ),
262
+ patch("shrinkray.passes.clangdelta.subprocess.run", return_value=mock_result),
259
263
  ):
260
- with patch(
261
- "shrinkray.passes.clangdelta.subprocess.run", return_value=mock_result
262
- ):
263
- assert clang_delta_works() is False
264
+ assert clang_delta_works() is False
264
265
 
265
266
 
266
267
  def test_find_clang_delta_when_found_in_path():
@@ -306,9 +307,8 @@ async def test_query_instances_raises_clang_delta_error():
306
307
  error.stdout = b"Some other error"
307
308
  error.stderr = b"More error info"
308
309
 
309
- with patch("trio.run_process", side_effect=error):
310
- with pytest.raises(ClangDeltaError):
311
- await cd.query_instances("rename-var", b"int main() {}")
310
+ with patch("trio.run_process", side_effect=error), pytest.raises(ClangDeltaError):
311
+ await cd.query_instances("rename-var", b"int main() {}")
312
312
 
313
313
 
314
314
  async def test_query_instances_handles_assertion_failure():
@@ -103,28 +103,36 @@ def test_validate_ui_returns_value_when_provided():
103
103
 
104
104
 
105
105
  def test_validate_ui_returns_textual_for_tty():
106
- with patch.object(sys.stdin, "isatty", return_value=True):
107
- with patch.object(sys.stdout, "isatty", return_value=True):
108
- result = validate_ui(None, None, None)
109
- assert result == UIType.textual
106
+ with (
107
+ patch.object(sys.stdin, "isatty", return_value=True),
108
+ patch.object(sys.stdout, "isatty", return_value=True),
109
+ ):
110
+ result = validate_ui(None, None, None)
111
+ assert result == UIType.textual
110
112
 
111
113
 
112
114
  def test_validate_ui_returns_basic_for_non_tty_stdin():
113
- with patch.object(sys.stdin, "isatty", return_value=False):
114
- with patch.object(sys.stdout, "isatty", return_value=True):
115
- result = validate_ui(None, None, None)
116
- assert result == UIType.basic
115
+ with (
116
+ patch.object(sys.stdin, "isatty", return_value=False),
117
+ patch.object(sys.stdout, "isatty", return_value=True),
118
+ ):
119
+ result = validate_ui(None, None, None)
120
+ assert result == UIType.basic
117
121
 
118
122
 
119
123
  def test_validate_ui_returns_basic_for_non_tty_stdout():
120
- with patch.object(sys.stdin, "isatty", return_value=True):
121
- with patch.object(sys.stdout, "isatty", return_value=False):
122
- result = validate_ui(None, None, None)
123
- assert result == UIType.basic
124
+ with (
125
+ patch.object(sys.stdin, "isatty", return_value=True),
126
+ patch.object(sys.stdout, "isatty", return_value=False),
127
+ ):
128
+ result = validate_ui(None, None, None)
129
+ assert result == UIType.basic
124
130
 
125
131
 
126
132
  def test_validate_ui_returns_basic_for_non_tty_both():
127
- with patch.object(sys.stdin, "isatty", return_value=False):
128
- with patch.object(sys.stdout, "isatty", return_value=False):
129
- result = validate_ui(None, None, None)
130
- assert result == UIType.basic
133
+ with (
134
+ patch.object(sys.stdin, "isatty", return_value=False),
135
+ patch.object(sys.stdout, "isatty", return_value=False),
136
+ ):
137
+ result = validate_ui(None, None, None)
138
+ assert result == UIType.basic
@@ -41,6 +41,7 @@ def test_format_is_valid_for_unparseable():
41
41
 
42
42
  async def test_compose_returns_early_on_parse_error():
43
43
  """Test that compose returns early when parsing fails after problem changes."""
44
+
44
45
  # Create a format that parses strings starting with "VALID:" successfully
45
46
  class SelectiveFormat(Format[str, str]):
46
47
  def parse(self, input: str) -> str:
@@ -213,10 +213,14 @@ def test_negating_map_repr():
213
213
  nm[3] = 4
214
214
  r = repr(nm)
215
215
  # Should contain both positive and negative mappings
216
- assert "1" in r and "-1" in r
217
- assert "2" in r and "-2" in r
218
- assert "3" in r and "-3" in r
219
- assert "4" in r and "-4" in r
216
+ assert "1" in r
217
+ assert "-1" in r
218
+ assert "2" in r
219
+ assert "-2" in r
220
+ assert "3" in r
221
+ assert "-3" in r
222
+ assert "4" in r
223
+ assert "-4" in r
220
224
 
221
225
 
222
226
  def test_unit_propagator_duplicate_unit():
@@ -44,10 +44,12 @@ def test_find_python_command_checks_python_bin_directory():
44
44
 
45
45
 
46
46
  def test_find_python_command_returns_none_when_not_in_bin_dir():
47
- with patch("shrinkray.formatting.which", return_value=None):
48
- with patch("os.path.exists", return_value=False):
49
- result = find_python_command("nonexistent")
50
- assert result is None
47
+ with (
48
+ patch("shrinkray.formatting.which", return_value=None),
49
+ patch("os.path.exists", return_value=False),
50
+ ):
51
+ result = find_python_command("nonexistent")
52
+ assert result is None
51
53
 
52
54
 
53
55
  # === try_decode tests ===
@@ -266,7 +266,6 @@ def test_normalize_identifiers_identifier_disappears():
266
266
  def test_regex_pass_with_compiled_pattern():
267
267
  """Test regex_pass with an already compiled pattern."""
268
268
 
269
-
270
269
  # Create a pass with a pre-compiled pattern
271
270
  pattern = re.compile(rb"[0-9]+")
272
271
 
@@ -345,7 +344,6 @@ async def test_normalize_identifiers_target_disappears():
345
344
  This can happen in concurrent scenarios or with modified problem classes.
346
345
  """
347
346
 
348
-
349
347
  # Create a mock problem that changes its current_test_case
350
348
  mock_problem = MagicMock()
351
349
  mock_problem.work = WorkContext(parallelism=1)