codefix-env 0.2.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.
- codefix_env/__init__.py +80 -0
- codefix_env/cli.py +83 -0
- codefix_env/client.py +194 -0
- codefix_env/env.py +503 -0
- codefix_env/models.py +234 -0
- codefix_env/rewards.py +157 -0
- codefix_env/tasks/__init__.py +75 -0
- codefix_env/tasks/easy.py +244 -0
- codefix_env/tasks/hard.py +430 -0
- codefix_env/tasks/medium.py +342 -0
- codefix_env/utils/__init__.py +33 -0
- codefix_env/utils/metrics.py +165 -0
- codefix_env/utils/reward_model.py +79 -0
- codefix_env/utils/sandbox.py +392 -0
- codefix_env-0.2.1.dist-info/METADATA +663 -0
- codefix_env-0.2.1.dist-info/RECORD +20 -0
- codefix_env-0.2.1.dist-info/WHEEL +5 -0
- codefix_env-0.2.1.dist-info/entry_points.txt +2 -0
- codefix_env-0.2.1.dist-info/licenses/LICENSE +201 -0
- codefix_env-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Easy Tasks — Syntax errors, simple fixes, missing colons, wrong indentation.
|
|
3
|
+
Each task has 3+ test cases and a clear hint.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from codefix_env.models import BugCategory, Difficulty, Task, TestCase
|
|
9
|
+
|
|
10
|
+
EASY_TASKS: list[Task] = [
|
|
11
|
+
# ── Task E1: Missing return ──────────────────────────────────────────
|
|
12
|
+
Task(
|
|
13
|
+
id="easy-001-missing-return",
|
|
14
|
+
title="Missing Return Statement",
|
|
15
|
+
description="The function adds two numbers but never returns the result.",
|
|
16
|
+
difficulty=Difficulty.EASY,
|
|
17
|
+
bug_category=BugCategory.RETURN_BUG,
|
|
18
|
+
tags=["return", "arithmetic"],
|
|
19
|
+
max_steps=10,
|
|
20
|
+
buggy_code="""\
|
|
21
|
+
def add_numbers(a, b):
|
|
22
|
+
result = a + b
|
|
23
|
+
""",
|
|
24
|
+
solution_code="""\
|
|
25
|
+
def add_numbers(a, b):
|
|
26
|
+
result = a + b
|
|
27
|
+
return result
|
|
28
|
+
""",
|
|
29
|
+
hints=[
|
|
30
|
+
"The function computes a result but never sends it back to the caller.",
|
|
31
|
+
"You need a `return` statement at the end of the function.",
|
|
32
|
+
],
|
|
33
|
+
test_cases=[
|
|
34
|
+
TestCase(name="test_positive", code="assert add_numbers(2, 3) == 5"),
|
|
35
|
+
TestCase(name="test_negative", code="assert add_numbers(-1, -4) == -5"),
|
|
36
|
+
TestCase(name="test_zero", code="assert add_numbers(0, 0) == 0"),
|
|
37
|
+
TestCase(name="test_float", code="assert add_numbers(1.5, 2.5) == 4.0"),
|
|
38
|
+
],
|
|
39
|
+
),
|
|
40
|
+
# ── Task E2: Wrong comparison operator ──────────────────────────────
|
|
41
|
+
Task(
|
|
42
|
+
id="easy-002-wrong-operator",
|
|
43
|
+
title="Wrong Comparison Operator",
|
|
44
|
+
description="The is_even function uses assignment `=` instead of equality `==`.",
|
|
45
|
+
difficulty=Difficulty.EASY,
|
|
46
|
+
bug_category=BugCategory.WRONG_OPERATOR,
|
|
47
|
+
tags=["operators", "boolean"],
|
|
48
|
+
max_steps=8,
|
|
49
|
+
buggy_code="""\
|
|
50
|
+
def is_even(n):
|
|
51
|
+
return n % 2 = 0
|
|
52
|
+
""",
|
|
53
|
+
solution_code="""\
|
|
54
|
+
def is_even(n):
|
|
55
|
+
return n % 2 == 0
|
|
56
|
+
""",
|
|
57
|
+
hints=[
|
|
58
|
+
"Python uses `==` for comparison, `=` is only for assignment.",
|
|
59
|
+
"Look at the return expression carefully — one character is wrong.",
|
|
60
|
+
],
|
|
61
|
+
test_cases=[
|
|
62
|
+
TestCase(name="test_even_2", code="assert is_even(2) == True"),
|
|
63
|
+
TestCase(name="test_odd_3", code="assert is_even(3) == False"),
|
|
64
|
+
TestCase(name="test_zero", code="assert is_even(0) == True"),
|
|
65
|
+
TestCase(name="test_neg", code="assert is_even(-4) == True"),
|
|
66
|
+
],
|
|
67
|
+
),
|
|
68
|
+
# ── Task E3: Off-by-one in range ─────────────────────────────────────
|
|
69
|
+
Task(
|
|
70
|
+
id="easy-003-off-by-one-range",
|
|
71
|
+
title="Off-by-One in Range",
|
|
72
|
+
description="sum_to_n should sum numbers 1 through n inclusive, but misses the last number.",
|
|
73
|
+
difficulty=Difficulty.EASY,
|
|
74
|
+
bug_category=BugCategory.OFF_BY_ONE,
|
|
75
|
+
tags=["range", "loop", "off-by-one"],
|
|
76
|
+
max_steps=10,
|
|
77
|
+
buggy_code="""\
|
|
78
|
+
def sum_to_n(n):
|
|
79
|
+
total = 0
|
|
80
|
+
for i in range(1, n):
|
|
81
|
+
total += i
|
|
82
|
+
return total
|
|
83
|
+
""",
|
|
84
|
+
solution_code="""\
|
|
85
|
+
def sum_to_n(n):
|
|
86
|
+
total = 0
|
|
87
|
+
for i in range(1, n + 1):
|
|
88
|
+
total += i
|
|
89
|
+
return total
|
|
90
|
+
""",
|
|
91
|
+
hints=[
|
|
92
|
+
"Python's range(a, b) excludes b. Think about whether n should be included.",
|
|
93
|
+
"Change `range(1, n)` to include n itself.",
|
|
94
|
+
],
|
|
95
|
+
test_cases=[
|
|
96
|
+
TestCase(name="test_sum_5", code="assert sum_to_n(5) == 15"),
|
|
97
|
+
TestCase(name="test_sum_10", code="assert sum_to_n(10) == 55"),
|
|
98
|
+
TestCase(name="test_sum_1", code="assert sum_to_n(1) == 1"),
|
|
99
|
+
TestCase(name="test_sum_0", code="assert sum_to_n(0) == 0"),
|
|
100
|
+
],
|
|
101
|
+
),
|
|
102
|
+
# ── Task E4: Wrong indentation ───────────────────────────────────────
|
|
103
|
+
Task(
|
|
104
|
+
id="easy-004-wrong-indent",
|
|
105
|
+
title="Wrong Indentation in Loop",
|
|
106
|
+
description="The return statement is inside the loop — it exits too early.",
|
|
107
|
+
difficulty=Difficulty.EASY,
|
|
108
|
+
bug_category=BugCategory.LOGIC,
|
|
109
|
+
tags=["indentation", "loop"],
|
|
110
|
+
max_steps=10,
|
|
111
|
+
buggy_code="""\
|
|
112
|
+
def find_max(numbers):
|
|
113
|
+
max_val = numbers[0]
|
|
114
|
+
for num in numbers:
|
|
115
|
+
if num > max_val:
|
|
116
|
+
max_val = num
|
|
117
|
+
return max_val
|
|
118
|
+
""",
|
|
119
|
+
solution_code="""\
|
|
120
|
+
def find_max(numbers):
|
|
121
|
+
max_val = numbers[0]
|
|
122
|
+
for num in numbers:
|
|
123
|
+
if num > max_val:
|
|
124
|
+
max_val = num
|
|
125
|
+
return max_val
|
|
126
|
+
""",
|
|
127
|
+
hints=[
|
|
128
|
+
"The return statement is indented inside the for loop — it exits after checking the first element.",
|
|
129
|
+
"De-indent `return max_val` by one level so it runs after the loop finishes.",
|
|
130
|
+
],
|
|
131
|
+
test_cases=[
|
|
132
|
+
TestCase(name="test_basic", code="assert find_max([3, 1, 4, 1, 5, 9]) == 9"),
|
|
133
|
+
TestCase(name="test_sorted", code="assert find_max([1, 2, 3, 4, 5]) == 5"),
|
|
134
|
+
TestCase(name="test_single", code="assert find_max([42]) == 42"),
|
|
135
|
+
TestCase(name="test_negative", code="assert find_max([-5, -1, -3]) == -1"),
|
|
136
|
+
],
|
|
137
|
+
),
|
|
138
|
+
# ── Task E5: Missing colon ───────────────────────────────────────────
|
|
139
|
+
Task(
|
|
140
|
+
id="easy-005-missing-colon",
|
|
141
|
+
title="Missing Colon in Function Definition",
|
|
142
|
+
description="A function definition is missing its colon, causing a SyntaxError.",
|
|
143
|
+
difficulty=Difficulty.EASY,
|
|
144
|
+
bug_category=BugCategory.SYNTAX,
|
|
145
|
+
tags=["syntax", "colon"],
|
|
146
|
+
max_steps=8,
|
|
147
|
+
buggy_code="""\
|
|
148
|
+
def greet(name)
|
|
149
|
+
return f"Hello, {name}!"
|
|
150
|
+
""",
|
|
151
|
+
solution_code="""\
|
|
152
|
+
def greet(name):
|
|
153
|
+
return f"Hello, {name}!"
|
|
154
|
+
""",
|
|
155
|
+
hints=[
|
|
156
|
+
"Every `def` statement must end with a colon `:`.",
|
|
157
|
+
],
|
|
158
|
+
test_cases=[
|
|
159
|
+
TestCase(name="test_hello", code='assert greet("Alice") == "Hello, Alice!"'),
|
|
160
|
+
TestCase(name="test_empty", code='assert greet("") == "Hello, !"'),
|
|
161
|
+
],
|
|
162
|
+
),
|
|
163
|
+
# ── Task E6: String not converted to int ────────────────────────────
|
|
164
|
+
Task(
|
|
165
|
+
id="easy-006-type-mismatch",
|
|
166
|
+
title="Type Mismatch — String + Integer",
|
|
167
|
+
description="The function tries to add a string to an integer without converting.",
|
|
168
|
+
difficulty=Difficulty.EASY,
|
|
169
|
+
bug_category=BugCategory.TYPE_ERROR,
|
|
170
|
+
tags=["type", "casting"],
|
|
171
|
+
max_steps=10,
|
|
172
|
+
buggy_code="""\
|
|
173
|
+
def add_age(base_age, years_str):
|
|
174
|
+
return base_age + years_str
|
|
175
|
+
""",
|
|
176
|
+
solution_code="""\
|
|
177
|
+
def add_age(base_age, years_str):
|
|
178
|
+
return base_age + int(years_str)
|
|
179
|
+
""",
|
|
180
|
+
hints=[
|
|
181
|
+
"years_str is a string. You need to convert it to int before adding.",
|
|
182
|
+
"Use `int(years_str)` to convert.",
|
|
183
|
+
],
|
|
184
|
+
test_cases=[
|
|
185
|
+
TestCase(name="test_basic", code='assert add_age(25, "5") == 30'),
|
|
186
|
+
TestCase(name="test_zero", code='assert add_age(30, "0") == 30'),
|
|
187
|
+
TestCase(name="test_large", code='assert add_age(0, "100") == 100'),
|
|
188
|
+
],
|
|
189
|
+
),
|
|
190
|
+
# ── Task E7: Wrong list index ────────────────────────────────────────
|
|
191
|
+
Task(
|
|
192
|
+
id="easy-007-wrong-index",
|
|
193
|
+
title="Wrong List Index",
|
|
194
|
+
description="get_last should return the last element but uses index 0.",
|
|
195
|
+
difficulty=Difficulty.EASY,
|
|
196
|
+
bug_category=BugCategory.INDEX_ERROR,
|
|
197
|
+
tags=["list", "index"],
|
|
198
|
+
max_steps=8,
|
|
199
|
+
buggy_code="""\
|
|
200
|
+
def get_last(items):
|
|
201
|
+
return items[0]
|
|
202
|
+
""",
|
|
203
|
+
solution_code="""\
|
|
204
|
+
def get_last(items):
|
|
205
|
+
return items[-1]
|
|
206
|
+
""",
|
|
207
|
+
hints=[
|
|
208
|
+
"Python index -1 refers to the last element of a list.",
|
|
209
|
+
],
|
|
210
|
+
test_cases=[
|
|
211
|
+
TestCase(name="test_basic", code="assert get_last([1, 2, 3]) == 3"),
|
|
212
|
+
TestCase(name="test_single", code="assert get_last(['x']) == 'x'"),
|
|
213
|
+
TestCase(name="test_strings", code="assert get_last(['a', 'b', 'c']) == 'c'"),
|
|
214
|
+
],
|
|
215
|
+
),
|
|
216
|
+
# ── Task E8: Boolean logic inverted ─────────────────────────────────
|
|
217
|
+
Task(
|
|
218
|
+
id="easy-008-inverted-bool",
|
|
219
|
+
title="Inverted Boolean Condition",
|
|
220
|
+
description="is_adult returns True for ages under 18 and False for adults.",
|
|
221
|
+
difficulty=Difficulty.EASY,
|
|
222
|
+
bug_category=BugCategory.LOGIC,
|
|
223
|
+
tags=["boolean", "condition"],
|
|
224
|
+
max_steps=8,
|
|
225
|
+
buggy_code="""\
|
|
226
|
+
def is_adult(age):
|
|
227
|
+
return age < 18
|
|
228
|
+
""",
|
|
229
|
+
solution_code="""\
|
|
230
|
+
def is_adult(age):
|
|
231
|
+
return age >= 18
|
|
232
|
+
""",
|
|
233
|
+
hints=[
|
|
234
|
+
"The condition is backwards — an adult is someone 18 or older.",
|
|
235
|
+
"Change `<` to `>=`.",
|
|
236
|
+
],
|
|
237
|
+
test_cases=[
|
|
238
|
+
TestCase(name="test_adult", code="assert is_adult(18) == True"),
|
|
239
|
+
TestCase(name="test_child", code="assert is_adult(10) == False"),
|
|
240
|
+
TestCase(name="test_exactly", code="assert is_adult(17) == False"),
|
|
241
|
+
TestCase(name="test_old", code="assert is_adult(65) == True"),
|
|
242
|
+
],
|
|
243
|
+
),
|
|
244
|
+
]
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hard Tasks — Multi-function bugs, complex algorithms, subtle logic errors.
|
|
3
|
+
These tasks require understanding interactions between multiple functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from codefix_env.models import BugCategory, Difficulty, Task, TestCase
|
|
9
|
+
|
|
10
|
+
HARD_TASKS: list[Task] = [
|
|
11
|
+
# ── Task H1: LRU Cache — wrong eviction ─────────────────────────────
|
|
12
|
+
Task(
|
|
13
|
+
id="hard-001-lru-cache",
|
|
14
|
+
title="LRU Cache — Wrong Eviction Policy",
|
|
15
|
+
description=(
|
|
16
|
+
"A manual LRU cache implementation evicts the most recently used item "
|
|
17
|
+
"instead of the least recently used. The `get` method also fails to update "
|
|
18
|
+
"recency on a cache hit."
|
|
19
|
+
),
|
|
20
|
+
difficulty=Difficulty.HARD,
|
|
21
|
+
bug_category=BugCategory.MULTI_FUNCTION,
|
|
22
|
+
tags=["lru", "cache", "data-structures", "ordered-dict"],
|
|
23
|
+
max_steps=25,
|
|
24
|
+
buggy_code="""\
|
|
25
|
+
from collections import OrderedDict
|
|
26
|
+
|
|
27
|
+
class LRUCache:
|
|
28
|
+
def __init__(self, capacity: int):
|
|
29
|
+
self.capacity = capacity
|
|
30
|
+
self.cache = OrderedDict()
|
|
31
|
+
|
|
32
|
+
def get(self, key: int) -> int:
|
|
33
|
+
if key not in self.cache:
|
|
34
|
+
return -1
|
|
35
|
+
# BUG: does not move to end (not updating recency)
|
|
36
|
+
return self.cache[key]
|
|
37
|
+
|
|
38
|
+
def put(self, key: int, value: int) -> None:
|
|
39
|
+
if key in self.cache:
|
|
40
|
+
self.cache.move_to_end(key)
|
|
41
|
+
self.cache[key] = value
|
|
42
|
+
if len(self.cache) > self.capacity:
|
|
43
|
+
self.cache.popitem(last=True) # BUG: evicts MRU not LRU
|
|
44
|
+
""",
|
|
45
|
+
solution_code="""\
|
|
46
|
+
from collections import OrderedDict
|
|
47
|
+
|
|
48
|
+
class LRUCache:
|
|
49
|
+
def __init__(self, capacity: int):
|
|
50
|
+
self.capacity = capacity
|
|
51
|
+
self.cache = OrderedDict()
|
|
52
|
+
|
|
53
|
+
def get(self, key: int) -> int:
|
|
54
|
+
if key not in self.cache:
|
|
55
|
+
return -1
|
|
56
|
+
self.cache.move_to_end(key) # FIX: mark as recently used
|
|
57
|
+
return self.cache[key]
|
|
58
|
+
|
|
59
|
+
def put(self, key: int, value: int) -> None:
|
|
60
|
+
if key in self.cache:
|
|
61
|
+
self.cache.move_to_end(key)
|
|
62
|
+
self.cache[key] = value
|
|
63
|
+
if len(self.cache) > self.capacity:
|
|
64
|
+
self.cache.popitem(last=False) # FIX: evict LRU (first item)
|
|
65
|
+
""",
|
|
66
|
+
hints=[
|
|
67
|
+
"When `get` is called, the accessed item should become the most recently used.",
|
|
68
|
+
"To evict the *least* recently used, use `popitem(last=False)` — removes the first (oldest) item.",
|
|
69
|
+
"Both `get` and `put` have bugs. Fix them independently.",
|
|
70
|
+
],
|
|
71
|
+
test_cases=[
|
|
72
|
+
TestCase(
|
|
73
|
+
name="test_basic_put_get",
|
|
74
|
+
code="""\
|
|
75
|
+
cache = LRUCache(2)
|
|
76
|
+
cache.put(1, 1)
|
|
77
|
+
cache.put(2, 2)
|
|
78
|
+
assert cache.get(1) == 1
|
|
79
|
+
""",
|
|
80
|
+
),
|
|
81
|
+
TestCase(
|
|
82
|
+
name="test_eviction",
|
|
83
|
+
code="""\
|
|
84
|
+
cache = LRUCache(2)
|
|
85
|
+
cache.put(1, 1)
|
|
86
|
+
cache.put(2, 2)
|
|
87
|
+
cache.get(1) # 1 is now MRU
|
|
88
|
+
cache.put(3, 3) # evicts 2 (LRU)
|
|
89
|
+
assert cache.get(2) == -1
|
|
90
|
+
assert cache.get(3) == 3
|
|
91
|
+
""",
|
|
92
|
+
),
|
|
93
|
+
TestCase(
|
|
94
|
+
name="test_update_existing",
|
|
95
|
+
code="""\
|
|
96
|
+
cache = LRUCache(2)
|
|
97
|
+
cache.put(1, 1)
|
|
98
|
+
cache.put(2, 2)
|
|
99
|
+
cache.put(1, 10) # update key 1
|
|
100
|
+
assert cache.get(1) == 10
|
|
101
|
+
""",
|
|
102
|
+
),
|
|
103
|
+
TestCase(
|
|
104
|
+
name="test_miss",
|
|
105
|
+
code="""\
|
|
106
|
+
cache = LRUCache(1)
|
|
107
|
+
assert cache.get(99) == -1
|
|
108
|
+
""",
|
|
109
|
+
),
|
|
110
|
+
],
|
|
111
|
+
),
|
|
112
|
+
# ── Task H2: Graph BFS — wrong visited tracking ─────────────────────
|
|
113
|
+
Task(
|
|
114
|
+
id="hard-002-graph-bfs",
|
|
115
|
+
title="Graph BFS — Infinite Loop from Wrong Visited Tracking",
|
|
116
|
+
description=(
|
|
117
|
+
"BFS adds nodes to `visited` after dequeuing instead of before enqueuing, "
|
|
118
|
+
"causing nodes to be visited multiple times and potentially looping."
|
|
119
|
+
),
|
|
120
|
+
difficulty=Difficulty.HARD,
|
|
121
|
+
bug_category=BugCategory.ALGORITHM_BUG,
|
|
122
|
+
tags=["graph", "bfs", "visited"],
|
|
123
|
+
max_steps=25,
|
|
124
|
+
buggy_code="""\
|
|
125
|
+
from collections import deque
|
|
126
|
+
|
|
127
|
+
def bfs(graph, start):
|
|
128
|
+
visited = set()
|
|
129
|
+
queue = deque([start])
|
|
130
|
+
order = []
|
|
131
|
+
while queue:
|
|
132
|
+
node = queue.popleft()
|
|
133
|
+
if node not in visited:
|
|
134
|
+
visited.add(node)
|
|
135
|
+
order.append(node)
|
|
136
|
+
for neighbour in graph.get(node, []):
|
|
137
|
+
queue.append(neighbour) # BUG: should check visited before enqueuing
|
|
138
|
+
return order
|
|
139
|
+
""",
|
|
140
|
+
solution_code="""\
|
|
141
|
+
from collections import deque
|
|
142
|
+
|
|
143
|
+
def bfs(graph, start):
|
|
144
|
+
visited = set([start])
|
|
145
|
+
queue = deque([start])
|
|
146
|
+
order = []
|
|
147
|
+
while queue:
|
|
148
|
+
node = queue.popleft()
|
|
149
|
+
order.append(node)
|
|
150
|
+
for neighbour in graph.get(node, []):
|
|
151
|
+
if neighbour not in visited:
|
|
152
|
+
visited.add(neighbour)
|
|
153
|
+
queue.append(neighbour)
|
|
154
|
+
return order
|
|
155
|
+
""",
|
|
156
|
+
hints=[
|
|
157
|
+
"Nodes should be added to `visited` when they are *enqueued*, not when dequeued.",
|
|
158
|
+
"Initialise `visited` with the start node.",
|
|
159
|
+
],
|
|
160
|
+
test_cases=[
|
|
161
|
+
TestCase(
|
|
162
|
+
name="test_linear",
|
|
163
|
+
code="""\
|
|
164
|
+
g = {0: [1], 1: [2], 2: [3], 3: []}
|
|
165
|
+
assert bfs(g, 0) == [0, 1, 2, 3]
|
|
166
|
+
""",
|
|
167
|
+
),
|
|
168
|
+
TestCase(
|
|
169
|
+
name="test_branching",
|
|
170
|
+
code="""\
|
|
171
|
+
g = {0: [1, 2], 1: [3], 2: [3], 3: []}
|
|
172
|
+
result = bfs(g, 0)
|
|
173
|
+
assert result[0] == 0
|
|
174
|
+
assert set(result) == {0, 1, 2, 3}
|
|
175
|
+
assert len(result) == 4 # no duplicates
|
|
176
|
+
""",
|
|
177
|
+
),
|
|
178
|
+
TestCase(
|
|
179
|
+
name="test_single_node",
|
|
180
|
+
code="""\
|
|
181
|
+
g = {0: []}
|
|
182
|
+
assert bfs(g, 0) == [0]
|
|
183
|
+
""",
|
|
184
|
+
),
|
|
185
|
+
],
|
|
186
|
+
),
|
|
187
|
+
# ── Task H3: Decorator — wrong wraps usage ──────────────────────────
|
|
188
|
+
Task(
|
|
189
|
+
id="hard-003-decorator-bug",
|
|
190
|
+
title="Decorator — Lost Function Metadata",
|
|
191
|
+
description=(
|
|
192
|
+
"A timing decorator is missing `@functools.wraps(func)`, which causes the "
|
|
193
|
+
"wrapped function to lose its `__name__` and `__doc__`, breaking introspection."
|
|
194
|
+
),
|
|
195
|
+
difficulty=Difficulty.HARD,
|
|
196
|
+
bug_category=BugCategory.MULTI_FUNCTION,
|
|
197
|
+
tags=["decorator", "functools", "wraps", "metadata"],
|
|
198
|
+
max_steps=15,
|
|
199
|
+
buggy_code="""\
|
|
200
|
+
import functools
|
|
201
|
+
import time
|
|
202
|
+
|
|
203
|
+
def timer(func):
|
|
204
|
+
def wrapper(*args, **kwargs): # BUG: missing @functools.wraps(func)
|
|
205
|
+
start = time.perf_counter()
|
|
206
|
+
result = func(*args, **kwargs)
|
|
207
|
+
elapsed = time.perf_counter() - start
|
|
208
|
+
return result
|
|
209
|
+
return wrapper
|
|
210
|
+
|
|
211
|
+
@timer
|
|
212
|
+
def compute(n):
|
|
213
|
+
\"\"\"Compute n^2.\"\"\"
|
|
214
|
+
return n * n
|
|
215
|
+
""",
|
|
216
|
+
solution_code="""\
|
|
217
|
+
import functools
|
|
218
|
+
import time
|
|
219
|
+
|
|
220
|
+
def timer(func):
|
|
221
|
+
@functools.wraps(func)
|
|
222
|
+
def wrapper(*args, **kwargs):
|
|
223
|
+
start = time.perf_counter()
|
|
224
|
+
result = func(*args, **kwargs)
|
|
225
|
+
elapsed = time.perf_counter() - start
|
|
226
|
+
return result
|
|
227
|
+
return wrapper
|
|
228
|
+
|
|
229
|
+
@timer
|
|
230
|
+
def compute(n):
|
|
231
|
+
\"\"\"Compute n^2.\"\"\"
|
|
232
|
+
return n * n
|
|
233
|
+
""",
|
|
234
|
+
hints=[
|
|
235
|
+
"Without `@functools.wraps(func)`, the wrapper function replaces the original's metadata.",
|
|
236
|
+
"Add `@functools.wraps(func)` directly above `def wrapper`.",
|
|
237
|
+
],
|
|
238
|
+
test_cases=[
|
|
239
|
+
TestCase(name="test_result", code="assert compute(5) == 25"),
|
|
240
|
+
TestCase(name="test_name", code='assert compute.__name__ == "compute"'),
|
|
241
|
+
TestCase(name="test_doc", code='assert compute.__doc__ == "Compute n^2."'),
|
|
242
|
+
],
|
|
243
|
+
),
|
|
244
|
+
# ── Task H4: Stack with linked list — pop bug ───────────────────────
|
|
245
|
+
Task(
|
|
246
|
+
id="hard-004-linked-stack",
|
|
247
|
+
title="Linked List Stack — Pop Returns Wrong Value",
|
|
248
|
+
description=(
|
|
249
|
+
"A stack implemented with a linked list has a `pop` method that updates the head "
|
|
250
|
+
"pointer but returns the new head's value instead of the removed node's value."
|
|
251
|
+
),
|
|
252
|
+
difficulty=Difficulty.HARD,
|
|
253
|
+
bug_category=BugCategory.MULTI_FUNCTION,
|
|
254
|
+
tags=["linked-list", "stack", "data-structures"],
|
|
255
|
+
max_steps=25,
|
|
256
|
+
buggy_code="""\
|
|
257
|
+
class Node:
|
|
258
|
+
def __init__(self, val):
|
|
259
|
+
self.val = val
|
|
260
|
+
self.next = None
|
|
261
|
+
|
|
262
|
+
class Stack:
|
|
263
|
+
def __init__(self):
|
|
264
|
+
self.head = None
|
|
265
|
+
self.size = 0
|
|
266
|
+
|
|
267
|
+
def push(self, val):
|
|
268
|
+
node = Node(val)
|
|
269
|
+
node.next = self.head
|
|
270
|
+
self.head = node
|
|
271
|
+
self.size += 1
|
|
272
|
+
|
|
273
|
+
def pop(self):
|
|
274
|
+
if self.head is None:
|
|
275
|
+
raise IndexError("pop from empty stack")
|
|
276
|
+
self.head = self.head.next # BUG: loses the removed node before capturing value
|
|
277
|
+
self.size -= 1
|
|
278
|
+
return self.head.val if self.head else None
|
|
279
|
+
|
|
280
|
+
def peek(self):
|
|
281
|
+
if self.head is None:
|
|
282
|
+
raise IndexError("peek from empty stack")
|
|
283
|
+
return self.head.val
|
|
284
|
+
|
|
285
|
+
def is_empty(self):
|
|
286
|
+
return self.head is None
|
|
287
|
+
""",
|
|
288
|
+
solution_code="""\
|
|
289
|
+
class Node:
|
|
290
|
+
def __init__(self, val):
|
|
291
|
+
self.val = val
|
|
292
|
+
self.next = None
|
|
293
|
+
|
|
294
|
+
class Stack:
|
|
295
|
+
def __init__(self):
|
|
296
|
+
self.head = None
|
|
297
|
+
self.size = 0
|
|
298
|
+
|
|
299
|
+
def push(self, val):
|
|
300
|
+
node = Node(val)
|
|
301
|
+
node.next = self.head
|
|
302
|
+
self.head = node
|
|
303
|
+
self.size += 1
|
|
304
|
+
|
|
305
|
+
def pop(self):
|
|
306
|
+
if self.head is None:
|
|
307
|
+
raise IndexError("pop from empty stack")
|
|
308
|
+
removed_val = self.head.val # FIX: capture value before moving head
|
|
309
|
+
self.head = self.head.next
|
|
310
|
+
self.size -= 1
|
|
311
|
+
return removed_val
|
|
312
|
+
|
|
313
|
+
def peek(self):
|
|
314
|
+
if self.head is None:
|
|
315
|
+
raise IndexError("peek from empty stack")
|
|
316
|
+
return self.head.val
|
|
317
|
+
|
|
318
|
+
def is_empty(self):
|
|
319
|
+
return self.head is None
|
|
320
|
+
""",
|
|
321
|
+
hints=[
|
|
322
|
+
"You need to save the current head's value before moving `self.head` to the next node.",
|
|
323
|
+
"Store `removed_val = self.head.val` before updating `self.head`.",
|
|
324
|
+
],
|
|
325
|
+
test_cases=[
|
|
326
|
+
TestCase(
|
|
327
|
+
name="test_push_pop",
|
|
328
|
+
code="""\
|
|
329
|
+
s = Stack()
|
|
330
|
+
s.push(1); s.push(2); s.push(3)
|
|
331
|
+
assert s.pop() == 3
|
|
332
|
+
assert s.pop() == 2
|
|
333
|
+
assert s.pop() == 1
|
|
334
|
+
""",
|
|
335
|
+
),
|
|
336
|
+
TestCase(
|
|
337
|
+
name="test_peek",
|
|
338
|
+
code="""\
|
|
339
|
+
s = Stack()
|
|
340
|
+
s.push(42)
|
|
341
|
+
assert s.peek() == 42
|
|
342
|
+
assert s.size == 1
|
|
343
|
+
""",
|
|
344
|
+
),
|
|
345
|
+
TestCase(
|
|
346
|
+
name="test_empty_pop",
|
|
347
|
+
code="""\
|
|
348
|
+
s = Stack()
|
|
349
|
+
try:
|
|
350
|
+
s.pop()
|
|
351
|
+
assert False, "Should have raised"
|
|
352
|
+
except IndexError:
|
|
353
|
+
pass
|
|
354
|
+
""",
|
|
355
|
+
),
|
|
356
|
+
TestCase(
|
|
357
|
+
name="test_size_tracking",
|
|
358
|
+
code="""\
|
|
359
|
+
s = Stack()
|
|
360
|
+
s.push(1); s.push(2)
|
|
361
|
+
s.pop()
|
|
362
|
+
assert s.size == 1
|
|
363
|
+
""",
|
|
364
|
+
),
|
|
365
|
+
],
|
|
366
|
+
),
|
|
367
|
+
# ── Task H5: Tokenizer — wrong string parsing ────────────────────────
|
|
368
|
+
Task(
|
|
369
|
+
id="hard-005-tokenizer",
|
|
370
|
+
title="Mini Tokenizer — String Literal Handling Bug",
|
|
371
|
+
description=(
|
|
372
|
+
"A simple expression tokenizer fails on string literals because it doesn't "
|
|
373
|
+
"handle quoted strings — it splits on spaces inside quotes."
|
|
374
|
+
),
|
|
375
|
+
difficulty=Difficulty.HARD,
|
|
376
|
+
bug_category=BugCategory.MULTI_FUNCTION,
|
|
377
|
+
tags=["parsing", "tokenizer", "string-literals"],
|
|
378
|
+
max_steps=25,
|
|
379
|
+
buggy_code="""\
|
|
380
|
+
def tokenize(expr):
|
|
381
|
+
\"\"\"Tokenize a simple expression into a list of tokens.\"\"\"
|
|
382
|
+
tokens = []
|
|
383
|
+
current = ''
|
|
384
|
+
for ch in expr:
|
|
385
|
+
if ch == ' ':
|
|
386
|
+
if current:
|
|
387
|
+
tokens.append(current)
|
|
388
|
+
current = ''
|
|
389
|
+
else:
|
|
390
|
+
current += ch
|
|
391
|
+
if current:
|
|
392
|
+
tokens.append(current)
|
|
393
|
+
return tokens
|
|
394
|
+
""",
|
|
395
|
+
solution_code="""\
|
|
396
|
+
def tokenize(expr):
|
|
397
|
+
\"\"\"Tokenize a simple expression into a list of tokens.\"\"\"
|
|
398
|
+
tokens = []
|
|
399
|
+
current = ''
|
|
400
|
+
in_string = False
|
|
401
|
+
for ch in expr:
|
|
402
|
+
if ch == '\"' :
|
|
403
|
+
in_string = not in_string
|
|
404
|
+
current += ch
|
|
405
|
+
elif ch == ' ' and not in_string:
|
|
406
|
+
if current:
|
|
407
|
+
tokens.append(current)
|
|
408
|
+
current = ''
|
|
409
|
+
else:
|
|
410
|
+
current += ch
|
|
411
|
+
if current:
|
|
412
|
+
tokens.append(current)
|
|
413
|
+
return tokens
|
|
414
|
+
""",
|
|
415
|
+
hints=[
|
|
416
|
+
"The tokenizer doesn't track whether it's inside a quoted string.",
|
|
417
|
+
'Add an `in_string` flag that toggles when a `"` is encountered.',
|
|
418
|
+
"Only split on spaces when `in_string` is False.",
|
|
419
|
+
],
|
|
420
|
+
test_cases=[
|
|
421
|
+
TestCase(name="test_simple", code='assert tokenize("a + b") == ["a", "+", "b"]'),
|
|
422
|
+
TestCase(
|
|
423
|
+
name="test_string_literal",
|
|
424
|
+
code="assert tokenize('print \"hello world\"') == ['print', '\"hello world\"']",
|
|
425
|
+
),
|
|
426
|
+
TestCase(name="test_no_spaces", code='assert tokenize("abc") == ["abc"]'),
|
|
427
|
+
TestCase(name="test_empty", code='assert tokenize("") == []'),
|
|
428
|
+
],
|
|
429
|
+
),
|
|
430
|
+
]
|