shrinkray 0.0.0__py3-none-any.whl → 25.12.26__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.
@@ -1,4 +1,5 @@
1
- from typing import Any, AnyStr, Callable
1
+ from collections.abc import Callable
2
+ from typing import Any, cast
2
3
 
3
4
  import libcst
4
5
  import libcst.matchers as m
@@ -8,7 +9,7 @@ from shrinkray.problem import ReductionProblem
8
9
  from shrinkray.work import NotFound
9
10
 
10
11
 
11
- def is_python(source: AnyStr) -> bool:
12
+ def is_python[AnyStr: (str, bytes)](source: AnyStr) -> bool:
12
13
  try:
13
14
  libcst.parse_module(source)
14
15
  return True
@@ -27,10 +28,20 @@ async def libcst_transform(
27
28
  Replacement,
28
29
  ],
29
30
  ) -> None:
31
+ """Apply a LibCST transformation to matching nodes in Python source.
32
+
33
+ Parses the test case as Python, finds all nodes matching the given
34
+ matcher, and applies the transformer function to each. Uses binary
35
+ search to efficiently find which transformations maintain interestingness.
36
+ """
37
+
30
38
  class CM(codemod.VisitorBasedCodemodCommand):
31
- def __init__(self, context: codemod.CodemodContext, target_index: int):
39
+ def __init__(
40
+ self, context: codemod.CodemodContext, start_index: int, end_index: int
41
+ ):
32
42
  super().__init__(context)
33
- self.target_index = target_index
43
+ self.start_index = start_index
44
+ self.end_index = end_index
34
45
  self.current_index = 0
35
46
  self.fired = False
36
47
 
@@ -39,8 +50,9 @@ async def libcst_transform(
39
50
  # and we use this generically in a way that makes it hard to type correctly.
40
51
  @m.leave(matcher)
41
52
  def maybe_change_node(self, _, updated_node): # type: ignore
42
- if self.current_index == self.target_index:
53
+ if self.start_index <= self.current_index < self.end_index:
43
54
  self.fired = True
55
+ self.current_index += 1
44
56
  return transformer(updated_node)
45
57
  else:
46
58
  self.current_index += 1
@@ -53,14 +65,14 @@ async def libcst_transform(
53
65
 
54
66
  context = codemod.CodemodContext()
55
67
 
56
- counting_mod = CM(context, -1)
68
+ counting_mod = CM(context, -1, -1)
57
69
  counting_mod.transform_module(module)
58
70
 
59
71
  n = counting_mod.current_index + 1
60
72
 
61
- async def can_apply(i: int) -> bool:
73
+ async def can_apply(start: int, end: int) -> bool:
62
74
  nonlocal n
63
- if i >= n:
75
+ if start >= n:
64
76
  return False
65
77
  initial_test_case = problem.current_test_case
66
78
  try:
@@ -69,7 +81,7 @@ async def libcst_transform(
69
81
  n = 0
70
82
  return False
71
83
 
72
- codemod_i = CM(context, i)
84
+ codemod_i = CM(context, start, end)
73
85
  try:
74
86
  transformed = codemod_i.transform_module(module)
75
87
  except libcst.CSTValidationError:
@@ -80,7 +92,7 @@ async def libcst_transform(
80
92
  raise
81
93
 
82
94
  if not codemod_i.fired:
83
- n = i
95
+ n = start
84
96
  return False
85
97
 
86
98
  transformed_test_case = transformed.code.encode(transformed.encoding)
@@ -95,12 +107,22 @@ async def libcst_transform(
95
107
  i = 0
96
108
  while i < n:
97
109
  try:
98
- i = await problem.work.find_first_value(range(i, n), can_apply)
110
+ i = await problem.work.find_first_value(
111
+ range(i, n), lambda i: can_apply(i, i + 1)
112
+ )
113
+ await problem.work.find_large_integer(lambda k: can_apply(i + 1, i + 1 + k))
114
+ i += 1
99
115
  except NotFound:
100
116
  break
101
117
 
102
118
 
103
119
  async def lift_indented_constructs(problem: ReductionProblem[bytes]) -> None:
120
+ """Replace control structures with their body contents.
121
+
122
+ For if/while/try/with statements, removes the else/except clauses and
123
+ then tries to replace the entire construct with just its body. This
124
+ "lifts" the indented code out of its containing structure.
125
+ """
104
126
  await libcst_transform(
105
127
  problem,
106
128
  m.OneOf(m.While(), m.If(), m.Try()),
@@ -115,6 +137,11 @@ async def lift_indented_constructs(problem: ReductionProblem[bytes]) -> None:
115
137
 
116
138
 
117
139
  async def delete_statements(problem: ReductionProblem[bytes]) -> None:
140
+ """Try to delete simple statement lines from Python source.
141
+
142
+ Removes individual statements (assignments, expressions, imports, etc.)
143
+ one at a time to find which can be eliminated.
144
+ """
118
145
  await libcst_transform(
119
146
  problem,
120
147
  m.SimpleStatementLine(),
@@ -123,6 +150,11 @@ async def delete_statements(problem: ReductionProblem[bytes]) -> None:
123
150
 
124
151
 
125
152
  async def replace_statements_with_pass(problem: ReductionProblem[bytes]) -> None:
153
+ """Replace statement lines with 'pass' statements.
154
+
155
+ Useful when a statement can't be deleted outright (e.g., it's the only
156
+ statement in a block) but can be replaced with a no-op.
157
+ """
126
158
  await libcst_transform(
127
159
  problem,
128
160
  m.SimpleStatementLine(),
@@ -134,6 +166,11 @@ ELLIPSIS_STATEMENT = libcst.parse_statement("...")
134
166
 
135
167
 
136
168
  async def replace_bodies_with_ellipsis(problem: ReductionProblem[bytes]) -> None:
169
+ """Replace indented block bodies with '...' (ellipsis).
170
+
171
+ Replaces function bodies, class bodies, and other indented blocks
172
+ with just an ellipsis statement. Useful for stubbing out implementations.
173
+ """
137
174
  await libcst_transform(
138
175
  problem,
139
176
  m.IndentedBlock(),
@@ -142,6 +179,12 @@ async def replace_bodies_with_ellipsis(problem: ReductionProblem[bytes]) -> None
142
179
 
143
180
 
144
181
  async def strip_annotations(problem: ReductionProblem[bytes]) -> None:
182
+ """Remove type annotations from Python source.
183
+
184
+ Strips return type annotations from functions, parameter annotations,
185
+ and converts annotated assignments to plain assignments. Type annotations
186
+ are often unnecessary for reproducing bugs.
187
+ """
145
188
  await libcst_transform(
146
189
  problem,
147
190
  m.FunctionDef(),
@@ -152,19 +195,19 @@ async def strip_annotations(problem: ReductionProblem[bytes]) -> None:
152
195
  m.Param(),
153
196
  lambda x: x.with_changes(annotation=None),
154
197
  )
155
- await libcst_transform(
156
- problem,
157
- m.AnnAssign(),
158
- lambda x: (
159
- libcst.Assign(
160
- targets=[libcst.AssignTarget(target=x.target)],
161
- value=x.value,
162
- semicolon=x.semicolon,
198
+
199
+ def ann_assign_to_assign(x: libcst.CSTNode) -> Replacement:
200
+ ann = cast(libcst.AnnAssign, x)
201
+ if ann.value is not None:
202
+ return libcst.Assign(
203
+ targets=[libcst.AssignTarget(target=ann.target)],
204
+ value=ann.value,
205
+ semicolon=ann.semicolon,
163
206
  )
164
- if x.value
165
- else libcst.RemoveFromParent()
166
- ),
167
- )
207
+ else:
208
+ return libcst.RemoveFromParent()
209
+
210
+ await libcst_transform(problem, m.AnnAssign(), ann_assign_to_assign)
168
211
 
169
212
 
170
213
  PYTHON_PASSES = [