claude-dev-cli 0.6.0__py3-none-any.whl → 0.8.0__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.
Potentially problematic release.
This version of claude-dev-cli might be problematic. Click here for more details.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +452 -22
- claude_dev_cli/config.py +6 -0
- claude_dev_cli/context.py +494 -0
- claude_dev_cli/warp_integration.py +243 -0
- claude_dev_cli/workflows.py +340 -0
- {claude_dev_cli-0.6.0.dist-info → claude_dev_cli-0.8.0.dist-info}/METADATA +50 -5
- {claude_dev_cli-0.6.0.dist-info → claude_dev_cli-0.8.0.dist-info}/RECORD +12 -9
- {claude_dev_cli-0.6.0.dist-info → claude_dev_cli-0.8.0.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.6.0.dist-info → claude_dev_cli-0.8.0.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.6.0.dist-info → claude_dev_cli-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.6.0.dist-info → claude_dev_cli-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Intelligent context gathering for AI operations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Set
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ContextItem:
|
|
14
|
+
"""A single piece of context information."""
|
|
15
|
+
type: str # 'file', 'git', 'dependency', 'error'
|
|
16
|
+
content: str
|
|
17
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
def format_for_prompt(self) -> str:
|
|
20
|
+
"""Format this context item for inclusion in a prompt."""
|
|
21
|
+
if self.type == 'file':
|
|
22
|
+
path = self.metadata.get('path', 'unknown')
|
|
23
|
+
return f"# File: {path}\n\n{self.content}\n"
|
|
24
|
+
elif self.type == 'git':
|
|
25
|
+
return f"# Git Context\n\n{self.content}\n"
|
|
26
|
+
elif self.type == 'dependency':
|
|
27
|
+
return f"# Dependencies\n\n{self.content}\n"
|
|
28
|
+
elif self.type == 'error':
|
|
29
|
+
return f"# Error Context\n\n{self.content}\n"
|
|
30
|
+
else:
|
|
31
|
+
return self.content
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Context:
|
|
36
|
+
"""Collection of context items."""
|
|
37
|
+
items: List[ContextItem] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
def add(self, item: ContextItem) -> None:
|
|
40
|
+
"""Add a context item."""
|
|
41
|
+
self.items.append(item)
|
|
42
|
+
|
|
43
|
+
def format_for_prompt(self) -> str:
|
|
44
|
+
"""Format all context items for inclusion in a prompt."""
|
|
45
|
+
if not self.items:
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
parts = ["# Context Information\n"]
|
|
49
|
+
for item in self.items:
|
|
50
|
+
parts.append(item.format_for_prompt())
|
|
51
|
+
|
|
52
|
+
return "\n".join(parts)
|
|
53
|
+
|
|
54
|
+
def get_by_type(self, context_type: str) -> List[ContextItem]:
|
|
55
|
+
"""Get all context items of a specific type."""
|
|
56
|
+
return [item for item in self.items if item.type == context_type]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GitContext:
|
|
60
|
+
"""Gather Git-related context."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, cwd: Optional[Path] = None):
|
|
63
|
+
self.cwd = cwd or Path.cwd()
|
|
64
|
+
|
|
65
|
+
def is_git_repo(self) -> bool:
|
|
66
|
+
"""Check if current directory is a git repository."""
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
['git', 'rev-parse', '--git-dir'],
|
|
70
|
+
cwd=self.cwd,
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True
|
|
73
|
+
)
|
|
74
|
+
return result.returncode == 0
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def get_current_branch(self) -> Optional[str]:
|
|
79
|
+
"""Get the current git branch."""
|
|
80
|
+
try:
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
83
|
+
cwd=self.cwd,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
check=True
|
|
87
|
+
)
|
|
88
|
+
return result.stdout.strip()
|
|
89
|
+
except Exception:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def get_recent_commits(self, count: int = 5) -> List[Dict[str, str]]:
|
|
93
|
+
"""Get recent commit messages."""
|
|
94
|
+
try:
|
|
95
|
+
result = subprocess.run(
|
|
96
|
+
['git', '--no-pager', 'log', f'-{count}', '--pretty=format:%h|%s|%an|%ar'],
|
|
97
|
+
cwd=self.cwd,
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
check=True
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
commits = []
|
|
104
|
+
for line in result.stdout.strip().split('\n'):
|
|
105
|
+
if line:
|
|
106
|
+
parts = line.split('|', 3)
|
|
107
|
+
if len(parts) == 4:
|
|
108
|
+
commits.append({
|
|
109
|
+
'hash': parts[0],
|
|
110
|
+
'message': parts[1],
|
|
111
|
+
'author': parts[2],
|
|
112
|
+
'date': parts[3]
|
|
113
|
+
})
|
|
114
|
+
return commits
|
|
115
|
+
except Exception:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
def get_staged_diff(self) -> Optional[str]:
|
|
119
|
+
"""Get diff of staged changes."""
|
|
120
|
+
try:
|
|
121
|
+
result = subprocess.run(
|
|
122
|
+
['git', '--no-pager', 'diff', '--cached'],
|
|
123
|
+
cwd=self.cwd,
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
check=True
|
|
127
|
+
)
|
|
128
|
+
return result.stdout if result.stdout else None
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def get_unstaged_diff(self) -> Optional[str]:
|
|
133
|
+
"""Get diff of unstaged changes."""
|
|
134
|
+
try:
|
|
135
|
+
result = subprocess.run(
|
|
136
|
+
['git', '--no-pager', 'diff'],
|
|
137
|
+
cwd=self.cwd,
|
|
138
|
+
capture_output=True,
|
|
139
|
+
text=True,
|
|
140
|
+
check=True
|
|
141
|
+
)
|
|
142
|
+
return result.stdout if result.stdout else None
|
|
143
|
+
except Exception:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def get_modified_files(self) -> List[str]:
|
|
147
|
+
"""Get list of modified files."""
|
|
148
|
+
try:
|
|
149
|
+
result = subprocess.run(
|
|
150
|
+
['git', 'status', '--porcelain'],
|
|
151
|
+
cwd=self.cwd,
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
check=True
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
files = []
|
|
158
|
+
for line in result.stdout.strip().split('\n'):
|
|
159
|
+
if line:
|
|
160
|
+
# Format: "XY filename"
|
|
161
|
+
parts = line.strip().split(maxsplit=1)
|
|
162
|
+
if len(parts) == 2:
|
|
163
|
+
files.append(parts[1])
|
|
164
|
+
return files
|
|
165
|
+
except Exception:
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
def gather(self, include_diff: bool = False) -> ContextItem:
|
|
169
|
+
"""Gather all git context."""
|
|
170
|
+
parts = []
|
|
171
|
+
|
|
172
|
+
branch = self.get_current_branch()
|
|
173
|
+
if branch:
|
|
174
|
+
parts.append(f"Branch: {branch}")
|
|
175
|
+
|
|
176
|
+
commits = self.get_recent_commits(5)
|
|
177
|
+
if commits:
|
|
178
|
+
parts.append("\nRecent commits:")
|
|
179
|
+
for commit in commits:
|
|
180
|
+
parts.append(f" {commit['hash']} - {commit['message']} ({commit['date']})")
|
|
181
|
+
|
|
182
|
+
modified = self.get_modified_files()
|
|
183
|
+
if modified:
|
|
184
|
+
parts.append(f"\nModified files: {', '.join(modified[:10])}")
|
|
185
|
+
|
|
186
|
+
if include_diff:
|
|
187
|
+
staged = self.get_staged_diff()
|
|
188
|
+
if staged:
|
|
189
|
+
parts.append(f"\nStaged changes:\n{staged[:1000]}...") # Limit size
|
|
190
|
+
|
|
191
|
+
content = "\n".join(parts) if parts else "No git context available"
|
|
192
|
+
|
|
193
|
+
return ContextItem(
|
|
194
|
+
type='git',
|
|
195
|
+
content=content,
|
|
196
|
+
metadata={'branch': branch, 'modified_count': len(modified)}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class DependencyAnalyzer:
|
|
201
|
+
"""Analyze project dependencies and imports."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, project_root: Path):
|
|
204
|
+
self.project_root = project_root
|
|
205
|
+
|
|
206
|
+
def find_python_imports(self, file_path: Path) -> Set[str]:
|
|
207
|
+
"""Extract imports from a Python file."""
|
|
208
|
+
imports = set()
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
with open(file_path, 'r') as f:
|
|
212
|
+
tree = ast.parse(f.read())
|
|
213
|
+
|
|
214
|
+
for node in ast.walk(tree):
|
|
215
|
+
if isinstance(node, ast.Import):
|
|
216
|
+
for name in node.names:
|
|
217
|
+
imports.add(name.name.split('.')[0])
|
|
218
|
+
elif isinstance(node, ast.ImportFrom):
|
|
219
|
+
if node.module:
|
|
220
|
+
imports.add(node.module.split('.')[0])
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
return imports
|
|
225
|
+
|
|
226
|
+
def find_related_files(self, file_path: Path, max_depth: int = 2) -> List[Path]:
|
|
227
|
+
"""Find files related to the given file through imports."""
|
|
228
|
+
if not file_path.suffix == '.py':
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
related = []
|
|
232
|
+
imports = self.find_python_imports(file_path)
|
|
233
|
+
|
|
234
|
+
# Look for local modules
|
|
235
|
+
for imp in imports:
|
|
236
|
+
# Try as module file
|
|
237
|
+
module_file = self.project_root / f"{imp}.py"
|
|
238
|
+
if module_file.exists() and module_file != file_path:
|
|
239
|
+
related.append(module_file)
|
|
240
|
+
|
|
241
|
+
# Try as package
|
|
242
|
+
package_init = self.project_root / imp / "__init__.py"
|
|
243
|
+
if package_init.exists():
|
|
244
|
+
related.append(package_init)
|
|
245
|
+
|
|
246
|
+
return related[:5] # Limit to avoid too many files
|
|
247
|
+
|
|
248
|
+
def get_dependency_files(self) -> List[Path]:
|
|
249
|
+
"""Find dependency configuration files."""
|
|
250
|
+
files = []
|
|
251
|
+
|
|
252
|
+
# Python
|
|
253
|
+
for name in ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile']:
|
|
254
|
+
file = self.project_root / name
|
|
255
|
+
if file.exists():
|
|
256
|
+
files.append(file)
|
|
257
|
+
|
|
258
|
+
# Node.js
|
|
259
|
+
for name in ['package.json', 'package-lock.json']:
|
|
260
|
+
file = self.project_root / name
|
|
261
|
+
if file.exists():
|
|
262
|
+
files.append(file)
|
|
263
|
+
|
|
264
|
+
# Other
|
|
265
|
+
for name in ['Gemfile', 'go.mod', 'Cargo.toml']:
|
|
266
|
+
file = self.project_root / name
|
|
267
|
+
if file.exists():
|
|
268
|
+
files.append(file)
|
|
269
|
+
|
|
270
|
+
return files
|
|
271
|
+
|
|
272
|
+
def gather(self, target_file: Optional[Path] = None) -> ContextItem:
|
|
273
|
+
"""Gather dependency context."""
|
|
274
|
+
parts = []
|
|
275
|
+
|
|
276
|
+
# Include dependency files
|
|
277
|
+
dep_files = self.get_dependency_files()
|
|
278
|
+
if dep_files:
|
|
279
|
+
parts.append("Dependency files:")
|
|
280
|
+
for file in dep_files[:3]: # Limit
|
|
281
|
+
parts.append(f" - {file.name}")
|
|
282
|
+
try:
|
|
283
|
+
content = file.read_text()
|
|
284
|
+
# Include only relevant parts
|
|
285
|
+
if file.suffix == '.json':
|
|
286
|
+
data = json.loads(content)
|
|
287
|
+
if 'dependencies' in data:
|
|
288
|
+
parts.append(f" Dependencies: {', '.join(list(data['dependencies'].keys())[:10])}")
|
|
289
|
+
elif file.suffix == '.txt':
|
|
290
|
+
lines = content.split('\n')[:20]
|
|
291
|
+
parts.append(f" Requirements: {', '.join([l.split('==')[0] for l in lines if l and not l.startswith('#')])}")
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
# Related files if target specified
|
|
296
|
+
if target_file and target_file.exists():
|
|
297
|
+
related = self.find_related_files(target_file)
|
|
298
|
+
if related:
|
|
299
|
+
parts.append(f"\nRelated files for {target_file.name}:")
|
|
300
|
+
for file in related:
|
|
301
|
+
parts.append(f" - {file.relative_to(self.project_root)}")
|
|
302
|
+
|
|
303
|
+
content = "\n".join(parts) if parts else "No dependency context found"
|
|
304
|
+
|
|
305
|
+
return ContextItem(
|
|
306
|
+
type='dependency',
|
|
307
|
+
content=content,
|
|
308
|
+
metadata={'dependency_files': [str(f) for f in dep_files]}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class ErrorContext:
|
|
313
|
+
"""Parse and format error context."""
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def parse_traceback(error_text: str) -> Dict[str, Any]:
|
|
317
|
+
"""Parse Python traceback into structured data."""
|
|
318
|
+
lines = error_text.split('\n')
|
|
319
|
+
|
|
320
|
+
# Find traceback start
|
|
321
|
+
traceback_start = -1
|
|
322
|
+
for i, line in enumerate(lines):
|
|
323
|
+
if 'Traceback' in line:
|
|
324
|
+
traceback_start = i
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
if traceback_start == -1:
|
|
328
|
+
return {'raw': error_text}
|
|
329
|
+
|
|
330
|
+
# Extract frames
|
|
331
|
+
frames = []
|
|
332
|
+
current_frame = {}
|
|
333
|
+
|
|
334
|
+
for line in lines[traceback_start + 1:]:
|
|
335
|
+
if line.startswith(' File '):
|
|
336
|
+
if current_frame:
|
|
337
|
+
frames.append(current_frame)
|
|
338
|
+
|
|
339
|
+
# Parse: File "path", line X, in function
|
|
340
|
+
match = re.match(r'\s*File "([^"]+)", line (\d+), in (.+)', line)
|
|
341
|
+
if match:
|
|
342
|
+
current_frame = {
|
|
343
|
+
'file': match.group(1),
|
|
344
|
+
'line': int(match.group(2)),
|
|
345
|
+
'function': match.group(3)
|
|
346
|
+
}
|
|
347
|
+
elif line.startswith(' ') and current_frame:
|
|
348
|
+
current_frame['code'] = line.strip()
|
|
349
|
+
elif line and not line.startswith(' '):
|
|
350
|
+
# Error message
|
|
351
|
+
if current_frame:
|
|
352
|
+
frames.append(current_frame)
|
|
353
|
+
current_frame = {}
|
|
354
|
+
|
|
355
|
+
error_type = line.split(':')[0] if ':' in line else line
|
|
356
|
+
error_message = line.split(':', 1)[1].strip() if ':' in line else ''
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
'frames': frames,
|
|
360
|
+
'error_type': error_type,
|
|
361
|
+
'error_message': error_message,
|
|
362
|
+
'raw': error_text
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {'frames': frames, 'raw': error_text}
|
|
366
|
+
|
|
367
|
+
@staticmethod
|
|
368
|
+
def format_for_ai(error_text: str) -> str:
|
|
369
|
+
"""Format error for AI consumption."""
|
|
370
|
+
parsed = ErrorContext.parse_traceback(error_text)
|
|
371
|
+
|
|
372
|
+
if 'error_type' not in parsed:
|
|
373
|
+
return error_text
|
|
374
|
+
|
|
375
|
+
parts = [
|
|
376
|
+
f"Error Type: {parsed['error_type']}",
|
|
377
|
+
f"Error Message: {parsed.get('error_message', 'N/A')}",
|
|
378
|
+
"\nStack Trace:"
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
for i, frame in enumerate(parsed.get('frames', []), 1):
|
|
382
|
+
parts.append(f" {i}. {frame.get('file', 'unknown')}:{frame.get('line', '?')} in {frame.get('function', 'unknown')}")
|
|
383
|
+
if 'code' in frame:
|
|
384
|
+
parts.append(f" > {frame['code']}")
|
|
385
|
+
|
|
386
|
+
return "\n".join(parts)
|
|
387
|
+
|
|
388
|
+
def gather(self, error_text: str) -> ContextItem:
|
|
389
|
+
"""Gather error context."""
|
|
390
|
+
formatted = self.format_for_ai(error_text)
|
|
391
|
+
parsed = self.parse_traceback(error_text)
|
|
392
|
+
|
|
393
|
+
return ContextItem(
|
|
394
|
+
type='error',
|
|
395
|
+
content=formatted,
|
|
396
|
+
metadata=parsed
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class ContextGatherer:
|
|
401
|
+
"""Main context gathering coordinator."""
|
|
402
|
+
|
|
403
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
404
|
+
self.project_root = project_root or Path.cwd()
|
|
405
|
+
self.git = GitContext(self.project_root)
|
|
406
|
+
self.dependencies = DependencyAnalyzer(self.project_root)
|
|
407
|
+
self.error_parser = ErrorContext()
|
|
408
|
+
|
|
409
|
+
def gather_for_file(
|
|
410
|
+
self,
|
|
411
|
+
file_path: Path,
|
|
412
|
+
include_git: bool = True,
|
|
413
|
+
include_dependencies: bool = True,
|
|
414
|
+
include_related: bool = True
|
|
415
|
+
) -> Context:
|
|
416
|
+
"""Gather context for a specific file operation."""
|
|
417
|
+
context = Context()
|
|
418
|
+
|
|
419
|
+
# Add the file itself
|
|
420
|
+
if file_path.exists():
|
|
421
|
+
context.add(ContextItem(
|
|
422
|
+
type='file',
|
|
423
|
+
content=file_path.read_text(),
|
|
424
|
+
metadata={'path': str(file_path)}
|
|
425
|
+
))
|
|
426
|
+
|
|
427
|
+
# Add git context
|
|
428
|
+
if include_git and self.git.is_git_repo():
|
|
429
|
+
context.add(self.git.gather(include_diff=False))
|
|
430
|
+
|
|
431
|
+
# Add dependency context
|
|
432
|
+
if include_dependencies:
|
|
433
|
+
context.add(self.dependencies.gather(target_file=file_path if include_related else None))
|
|
434
|
+
|
|
435
|
+
return context
|
|
436
|
+
|
|
437
|
+
def gather_for_error(
|
|
438
|
+
self,
|
|
439
|
+
error_text: str,
|
|
440
|
+
file_path: Optional[Path] = None,
|
|
441
|
+
include_git: bool = True
|
|
442
|
+
) -> Context:
|
|
443
|
+
"""Gather context for error debugging."""
|
|
444
|
+
context = Context()
|
|
445
|
+
|
|
446
|
+
# Add error context
|
|
447
|
+
context.add(self.error_parser.gather(error_text))
|
|
448
|
+
|
|
449
|
+
# Add file if provided
|
|
450
|
+
if file_path and file_path.exists():
|
|
451
|
+
context.add(ContextItem(
|
|
452
|
+
type='file',
|
|
453
|
+
content=file_path.read_text(),
|
|
454
|
+
metadata={'path': str(file_path)}
|
|
455
|
+
))
|
|
456
|
+
|
|
457
|
+
# Add git context
|
|
458
|
+
if include_git and self.git.is_git_repo():
|
|
459
|
+
context.add(self.git.gather(include_diff=False))
|
|
460
|
+
|
|
461
|
+
return context
|
|
462
|
+
|
|
463
|
+
def gather_for_review(
|
|
464
|
+
self,
|
|
465
|
+
file_path: Path,
|
|
466
|
+
include_git: bool = True,
|
|
467
|
+
include_tests: bool = True
|
|
468
|
+
) -> Context:
|
|
469
|
+
"""Gather context for code review."""
|
|
470
|
+
context = self.gather_for_file(
|
|
471
|
+
file_path,
|
|
472
|
+
include_git=include_git,
|
|
473
|
+
include_dependencies=True,
|
|
474
|
+
include_related=True
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Try to find test file
|
|
478
|
+
if include_tests:
|
|
479
|
+
test_patterns = [
|
|
480
|
+
self.project_root / "tests" / f"test_{file_path.name}",
|
|
481
|
+
self.project_root / f"test_{file_path.name}",
|
|
482
|
+
file_path.parent / f"test_{file_path.name}"
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
for test_file in test_patterns:
|
|
486
|
+
if test_file.exists():
|
|
487
|
+
context.add(ContextItem(
|
|
488
|
+
type='file',
|
|
489
|
+
content=test_file.read_text(),
|
|
490
|
+
metadata={'path': str(test_file), 'is_test': True}
|
|
491
|
+
))
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
return context
|