toolplane-python-client 0.1.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.
- toolplane/__init__.py +106 -0
- toolplane/common/__init__.py +93 -0
- toolplane/common/base_config.py +129 -0
- toolplane/common/base_connection_manager.py +171 -0
- toolplane/common/base_session_manager.py +321 -0
- toolplane/common/base_tool_manager.py +347 -0
- toolplane/common/constants.py +47 -0
- toolplane/common/utils.py +310 -0
- toolplane/core/__init__.py +67 -0
- toolplane/core/config.py +107 -0
- toolplane/core/connection.py +285 -0
- toolplane/core/errors.py +298 -0
- toolplane/core/machine.py +480 -0
- toolplane/core/request.py +775 -0
- toolplane/core/session.py +332 -0
- toolplane/core/session_context.py +514 -0
- toolplane/core/task.py +130 -0
- toolplane/core/tool.py +329 -0
- toolplane/http_core/__init__.py +37 -0
- toolplane/http_core/http_config.py +97 -0
- toolplane/http_core/http_connection.py +409 -0
- toolplane/http_core/http_machine.py +298 -0
- toolplane/http_core/http_request.py +748 -0
- toolplane/http_core/http_session.py +348 -0
- toolplane/http_core/http_session_context.py +491 -0
- toolplane/http_core/http_task.py +101 -0
- toolplane/http_core/http_tool.py +400 -0
- toolplane/interfaces/__init__.py +27 -0
- toolplane/interfaces/client_interface.py +122 -0
- toolplane/interfaces/connection_interface.py +193 -0
- toolplane/interfaces/event_interface.py +290 -0
- toolplane/interfaces/request_interface.py +439 -0
- toolplane/interfaces/session_interface.py +288 -0
- toolplane/interfaces/tool_interface.py +441 -0
- toolplane/proto/__init__.py +0 -0
- toolplane/proto/service_pb2.py +315 -0
- toolplane/proto/service_pb2_grpc.py +2240 -0
- toolplane/provider_cli.py +268 -0
- toolplane/provider_registry.py +77 -0
- toolplane/provider_runtime.py +302 -0
- toolplane/toolkits/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/create_directory.py +94 -0
- toolplane/toolkits/standalone_tools/create_file.py +124 -0
- toolplane/toolkits/standalone_tools/file_search.py +229 -0
- toolplane/toolkits/standalone_tools/grep_search.py +372 -0
- toolplane/toolkits/standalone_tools/launcher.py +146 -0
- toolplane/toolkits/standalone_tools/list_dir.py +395 -0
- toolplane/toolkits/standalone_tools/read_file.py +346 -0
- toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
- toolplane/toolkits/standalone_tools/run_tests.py +66 -0
- toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
- toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
- toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
- toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
- toolplane/toolkits/swe/__init__.py +35 -0
- toolplane/toolkits/swe/create_directory.py +15 -0
- toolplane/toolkits/swe/create_file.py +15 -0
- toolplane/toolkits/swe/descriptions.py +273 -0
- toolplane/toolkits/swe/execute_bash.py +93 -0
- toolplane/toolkits/swe/file_editor.py +775 -0
- toolplane/toolkits/swe/file_search.py +16 -0
- toolplane/toolkits/swe/finish.py +50 -0
- toolplane/toolkits/swe/grep_search.py +19 -0
- toolplane/toolkits/swe/list_dir.py +407 -0
- toolplane/toolkits/swe/read_file.py +18 -0
- toolplane/toolkits/swe/replace_string_in_file.py +17 -0
- toolplane/toolkits/swe/search.py +260 -0
- toolplane/toolkits/swe/semantic_search.py +20 -0
- toolplane/toolkits/swe/str_replace_editor.py +647 -0
- toolplane/toolkits/swe/submit.py +29 -0
- toolplane/toolkits/swe/swe_toolkit.py +1296 -0
- toolplane/toolplane_client.py +686 -0
- toolplane/toolplane_http_client.py +681 -0
- toolplane/utils/__init__.py +3 -0
- toolplane/utils/schema.py +146 -0
- toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
- toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
- toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
- toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
- toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Description: Replace strings in files with enhanced features and safety checks.
|
|
4
|
+
|
|
5
|
+
This tool performs string replacements in files with validation, backup options,
|
|
6
|
+
and various safety features to prevent accidental data loss.
|
|
7
|
+
|
|
8
|
+
Parameters:
|
|
9
|
+
file_path (string, required): The absolute path to the file to edit.
|
|
10
|
+
old_string (string, required): The string to be replaced (must match exactly).
|
|
11
|
+
new_string (string, required): The replacement string.
|
|
12
|
+
create_backup (boolean, optional): Create a backup before editing (default: True).
|
|
13
|
+
dry_run (boolean, optional): Show what would be changed without making changes (default: False).
|
|
14
|
+
whole_word (boolean, optional): Only replace whole words (default: False).
|
|
15
|
+
ignore_case (boolean, optional): Perform case-insensitive matching (default: False).
|
|
16
|
+
max_replacements (integer, optional): Maximum number of replacements to make (default: unlimited).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def replace_string_in_file(
|
|
31
|
+
file_path: str,
|
|
32
|
+
old_string: str,
|
|
33
|
+
new_string: str,
|
|
34
|
+
create_backup: bool = True,
|
|
35
|
+
dry_run: bool = False,
|
|
36
|
+
whole_word: bool = False,
|
|
37
|
+
ignore_case: bool = False,
|
|
38
|
+
max_replacements: Optional[int] = None,
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Replace strings in a file with enhanced features and safety checks.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
file_path: The absolute path to the file to edit
|
|
45
|
+
old_string: The string to be replaced
|
|
46
|
+
new_string: The replacement string
|
|
47
|
+
create_backup: Create a backup before editing
|
|
48
|
+
dry_run: Show what would be changed without making changes
|
|
49
|
+
whole_word: Only replace whole words
|
|
50
|
+
ignore_case: Perform case-insensitive matching
|
|
51
|
+
max_replacements: Maximum number of replacements to make
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dictionary containing operation results and statistics
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
path = Path(file_path)
|
|
58
|
+
|
|
59
|
+
# Validate input
|
|
60
|
+
if not path.exists():
|
|
61
|
+
return {
|
|
62
|
+
"success": False,
|
|
63
|
+
"error": f"File does not exist: {file_path}",
|
|
64
|
+
"replacements_made": 0,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if not path.is_file():
|
|
68
|
+
return {
|
|
69
|
+
"success": False,
|
|
70
|
+
"error": f"Path is not a file: {file_path}",
|
|
71
|
+
"replacements_made": 0,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Read the file
|
|
75
|
+
try:
|
|
76
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
77
|
+
original_content = f.read()
|
|
78
|
+
except UnicodeDecodeError:
|
|
79
|
+
# Try with different encodings
|
|
80
|
+
for encoding in ["latin-1", "cp1252"]:
|
|
81
|
+
try:
|
|
82
|
+
with open(path, "r", encoding=encoding) as f:
|
|
83
|
+
original_content = f.read()
|
|
84
|
+
break
|
|
85
|
+
except UnicodeDecodeError:
|
|
86
|
+
continue
|
|
87
|
+
else:
|
|
88
|
+
return {
|
|
89
|
+
"success": False,
|
|
90
|
+
"error": f"Could not read file with any supported encoding: {file_path}",
|
|
91
|
+
"replacements_made": 0,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Prepare search pattern
|
|
95
|
+
if whole_word:
|
|
96
|
+
if ignore_case:
|
|
97
|
+
pattern = re.compile(
|
|
98
|
+
r"\b" + re.escape(old_string) + r"\b", re.IGNORECASE
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
pattern = re.compile(r"\b" + re.escape(old_string) + r"\b")
|
|
102
|
+
else:
|
|
103
|
+
if ignore_case:
|
|
104
|
+
pattern = re.compile(re.escape(old_string), re.IGNORECASE)
|
|
105
|
+
else:
|
|
106
|
+
pattern = re.compile(re.escape(old_string))
|
|
107
|
+
|
|
108
|
+
# Find all matches
|
|
109
|
+
matches = list(pattern.finditer(original_content))
|
|
110
|
+
|
|
111
|
+
if not matches:
|
|
112
|
+
return {
|
|
113
|
+
"success": True,
|
|
114
|
+
"message": f"No matches found for '{old_string}' in {file_path}",
|
|
115
|
+
"replacements_made": 0,
|
|
116
|
+
"dry_run": dry_run,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Apply max_replacements limit
|
|
120
|
+
if max_replacements is not None and len(matches) > max_replacements:
|
|
121
|
+
matches = matches[:max_replacements]
|
|
122
|
+
|
|
123
|
+
# Perform replacements
|
|
124
|
+
replacements_made = len(matches)
|
|
125
|
+
|
|
126
|
+
if dry_run:
|
|
127
|
+
# Show what would be changed
|
|
128
|
+
changes = []
|
|
129
|
+
for i, match in enumerate(matches[:5]): # Show first 5 matches
|
|
130
|
+
start = match.start()
|
|
131
|
+
end = match.end()
|
|
132
|
+
|
|
133
|
+
# Get context around the match
|
|
134
|
+
context_start = max(0, start - 50)
|
|
135
|
+
context_end = min(len(original_content), end + 50)
|
|
136
|
+
|
|
137
|
+
before = original_content[context_start:context_end]
|
|
138
|
+
after = before.replace(match.group(), new_string)
|
|
139
|
+
|
|
140
|
+
changes.append(
|
|
141
|
+
{
|
|
142
|
+
"match_number": i + 1,
|
|
143
|
+
"position": start,
|
|
144
|
+
"before": before,
|
|
145
|
+
"after": after,
|
|
146
|
+
"line_number": original_content[:start].count("\n") + 1,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"success": True,
|
|
152
|
+
"message": f"DRY RUN: Would replace {replacements_made} occurrences of '{old_string}' with '{new_string}' in {file_path}",
|
|
153
|
+
"replacements_made": replacements_made,
|
|
154
|
+
"dry_run": True,
|
|
155
|
+
"changes_preview": changes,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Create backup if requested
|
|
159
|
+
backup_path = None
|
|
160
|
+
if create_backup:
|
|
161
|
+
backup_path = create_file_backup(path)
|
|
162
|
+
|
|
163
|
+
# Apply replacements
|
|
164
|
+
new_content = pattern.sub(
|
|
165
|
+
new_string, original_content, count=max_replacements or 0
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Write the modified content
|
|
169
|
+
try:
|
|
170
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
171
|
+
f.write(new_content)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
# Restore backup if write fails
|
|
174
|
+
if backup_path and backup_path.exists():
|
|
175
|
+
shutil.copy2(backup_path, path)
|
|
176
|
+
raise e
|
|
177
|
+
|
|
178
|
+
result = {
|
|
179
|
+
"success": True,
|
|
180
|
+
"message": f"Successfully replaced {replacements_made} occurrences of '{old_string}' with '{new_string}' in {file_path}",
|
|
181
|
+
"replacements_made": replacements_made,
|
|
182
|
+
"dry_run": False,
|
|
183
|
+
"backup_created": backup_path is not None,
|
|
184
|
+
"backup_path": str(backup_path) if backup_path else None,
|
|
185
|
+
"original_size": len(original_content),
|
|
186
|
+
"new_size": len(new_content),
|
|
187
|
+
"size_change": len(new_content) - len(original_content),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
except PermissionError:
|
|
193
|
+
return {
|
|
194
|
+
"success": False,
|
|
195
|
+
"error": f"Permission denied: {file_path}",
|
|
196
|
+
"replacements_made": 0,
|
|
197
|
+
}
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return {
|
|
200
|
+
"success": False,
|
|
201
|
+
"error": f"Error processing file: {e}",
|
|
202
|
+
"replacements_made": 0,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def create_file_backup(file_path: Path) -> Optional[Path]:
|
|
207
|
+
"""Create a backup of the file with timestamp."""
|
|
208
|
+
try:
|
|
209
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
210
|
+
backup_path = file_path.with_suffix(f".backup_{timestamp}{file_path.suffix}")
|
|
211
|
+
|
|
212
|
+
shutil.copy2(file_path, backup_path)
|
|
213
|
+
return backup_path
|
|
214
|
+
|
|
215
|
+
except Exception:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def validate_old_string_uniqueness(file_path: str, old_string: str) -> Dict[str, Any]:
|
|
220
|
+
"""Validate that the old_string appears uniquely in the file."""
|
|
221
|
+
try:
|
|
222
|
+
path = Path(file_path)
|
|
223
|
+
|
|
224
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
225
|
+
content = f.read()
|
|
226
|
+
|
|
227
|
+
occurrences = content.count(old_string)
|
|
228
|
+
|
|
229
|
+
if occurrences == 0:
|
|
230
|
+
return {
|
|
231
|
+
"unique": False,
|
|
232
|
+
"count": 0,
|
|
233
|
+
"message": f"String '{old_string}' not found in file",
|
|
234
|
+
}
|
|
235
|
+
elif occurrences == 1:
|
|
236
|
+
return {
|
|
237
|
+
"unique": True,
|
|
238
|
+
"count": 1,
|
|
239
|
+
"message": f"String '{old_string}' found once (unique)",
|
|
240
|
+
}
|
|
241
|
+
else:
|
|
242
|
+
# Find all occurrences and their contexts
|
|
243
|
+
contexts = []
|
|
244
|
+
start = 0
|
|
245
|
+
for i in range(min(occurrences, 5)): # Show first 5 occurrences
|
|
246
|
+
pos = content.find(old_string, start)
|
|
247
|
+
if pos == -1:
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
context_start = max(0, pos - 50)
|
|
251
|
+
context_end = min(len(content), pos + len(old_string) + 50)
|
|
252
|
+
context = content[context_start:context_end]
|
|
253
|
+
line_number = content[:pos].count("\n") + 1
|
|
254
|
+
|
|
255
|
+
contexts.append(
|
|
256
|
+
{"position": pos, "line_number": line_number, "context": context}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
start = pos + 1
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"unique": False,
|
|
263
|
+
"count": occurrences,
|
|
264
|
+
"message": f"String '{old_string}' found {occurrences} times (not unique)",
|
|
265
|
+
"contexts": contexts,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return {
|
|
270
|
+
"unique": False,
|
|
271
|
+
"count": 0,
|
|
272
|
+
"message": f"Error checking uniqueness: {e}",
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main():
|
|
277
|
+
parser = argparse.ArgumentParser(
|
|
278
|
+
description="Replace strings in files with enhanced features and safety checks."
|
|
279
|
+
)
|
|
280
|
+
parser.add_argument(
|
|
281
|
+
"file_path", type=str, help="The absolute path to the file to edit"
|
|
282
|
+
)
|
|
283
|
+
parser.add_argument(
|
|
284
|
+
"old_string", type=str, help="The string to be replaced (must match exactly)"
|
|
285
|
+
)
|
|
286
|
+
parser.add_argument("new_string", type=str, help="The replacement string")
|
|
287
|
+
parser.add_argument(
|
|
288
|
+
"--create_backup",
|
|
289
|
+
action="store_true",
|
|
290
|
+
default=True,
|
|
291
|
+
help="Create a backup before editing (default: True)",
|
|
292
|
+
)
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"--no_backup",
|
|
295
|
+
action="store_true",
|
|
296
|
+
default=False,
|
|
297
|
+
help="Don't create a backup before editing",
|
|
298
|
+
)
|
|
299
|
+
parser.add_argument(
|
|
300
|
+
"--dry_run",
|
|
301
|
+
action="store_true",
|
|
302
|
+
default=False,
|
|
303
|
+
help="Show what would be changed without making changes (default: False)",
|
|
304
|
+
)
|
|
305
|
+
parser.add_argument(
|
|
306
|
+
"--whole_word",
|
|
307
|
+
action="store_true",
|
|
308
|
+
default=False,
|
|
309
|
+
help="Only replace whole words (default: False)",
|
|
310
|
+
)
|
|
311
|
+
parser.add_argument(
|
|
312
|
+
"--ignore_case",
|
|
313
|
+
action="store_true",
|
|
314
|
+
default=False,
|
|
315
|
+
help="Perform case-insensitive matching (default: False)",
|
|
316
|
+
)
|
|
317
|
+
parser.add_argument(
|
|
318
|
+
"--max_replacements",
|
|
319
|
+
type=int,
|
|
320
|
+
help="Maximum number of replacements to make (default: unlimited)",
|
|
321
|
+
)
|
|
322
|
+
parser.add_argument(
|
|
323
|
+
"--check_uniqueness",
|
|
324
|
+
action="store_true",
|
|
325
|
+
default=False,
|
|
326
|
+
help="Check if the old_string is unique before replacing",
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument(
|
|
329
|
+
"--output_format",
|
|
330
|
+
choices=["default", "json"],
|
|
331
|
+
default="default",
|
|
332
|
+
help="Output format (default: default)",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
args = parser.parse_args()
|
|
336
|
+
|
|
337
|
+
# Handle backup flag
|
|
338
|
+
create_backup = args.create_backup and not args.no_backup
|
|
339
|
+
|
|
340
|
+
# Check uniqueness if requested
|
|
341
|
+
if args.check_uniqueness:
|
|
342
|
+
uniqueness_result = validate_old_string_uniqueness(
|
|
343
|
+
args.file_path, args.old_string
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if args.output_format == "json":
|
|
347
|
+
import json
|
|
348
|
+
|
|
349
|
+
print(json.dumps(uniqueness_result, indent=2))
|
|
350
|
+
else:
|
|
351
|
+
print(uniqueness_result["message"])
|
|
352
|
+
|
|
353
|
+
if not uniqueness_result["unique"] and uniqueness_result["count"] > 1:
|
|
354
|
+
print(f"\nOccurrences found:")
|
|
355
|
+
for i, ctx in enumerate(uniqueness_result.get("contexts", []), 1):
|
|
356
|
+
print(
|
|
357
|
+
f"{i}. Line {ctx['line_number']}, Position {ctx['position']}:"
|
|
358
|
+
)
|
|
359
|
+
print(f" Context: {ctx['context'][:100]}...")
|
|
360
|
+
print()
|
|
361
|
+
|
|
362
|
+
if not uniqueness_result["unique"] and uniqueness_result["count"] > 1:
|
|
363
|
+
print(
|
|
364
|
+
"String is not unique. Use --dry_run to see all changes before applying."
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Perform replacement
|
|
368
|
+
result = replace_string_in_file(
|
|
369
|
+
args.file_path,
|
|
370
|
+
args.old_string,
|
|
371
|
+
args.new_string,
|
|
372
|
+
create_backup=create_backup,
|
|
373
|
+
dry_run=args.dry_run,
|
|
374
|
+
whole_word=args.whole_word,
|
|
375
|
+
ignore_case=args.ignore_case,
|
|
376
|
+
max_replacements=args.max_replacements,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if args.output_format == "json":
|
|
380
|
+
import json
|
|
381
|
+
|
|
382
|
+
print(json.dumps(result, indent=2))
|
|
383
|
+
else:
|
|
384
|
+
if result["success"]:
|
|
385
|
+
print(result["message"])
|
|
386
|
+
|
|
387
|
+
if args.dry_run and "changes_preview" in result:
|
|
388
|
+
print("\nChanges preview:")
|
|
389
|
+
for change in result["changes_preview"]:
|
|
390
|
+
print(
|
|
391
|
+
f"\nMatch {change['match_number']} at line {change['line_number']}:"
|
|
392
|
+
)
|
|
393
|
+
print(f"Before: {change['before']}")
|
|
394
|
+
print(f"After: {change['after']}")
|
|
395
|
+
|
|
396
|
+
if result.get("backup_created"):
|
|
397
|
+
print(f"Backup created: {result['backup_path']}")
|
|
398
|
+
|
|
399
|
+
else:
|
|
400
|
+
print(f"Error: {result['error']}", file=sys.stderr)
|
|
401
|
+
|
|
402
|
+
# Exit with error code if operation failed
|
|
403
|
+
sys.exit(0 if result["success"] else 1)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
if __name__ == "__main__":
|
|
407
|
+
main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test runner script for standalone tools.
|
|
4
|
+
|
|
5
|
+
This script provides an easy way to run the test suite with various options.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_tests(test_pattern=None, verbose=False, coverage=False, markers=None):
|
|
15
|
+
"""Run the test suite with specified options."""
|
|
16
|
+
cmd = [sys.executable, "-m", "pytest"]
|
|
17
|
+
|
|
18
|
+
if verbose:
|
|
19
|
+
cmd.append("-v")
|
|
20
|
+
|
|
21
|
+
if coverage:
|
|
22
|
+
cmd.extend(["--cov=.", "--cov-report=html", "--cov-report=term"])
|
|
23
|
+
|
|
24
|
+
if markers:
|
|
25
|
+
cmd.extend(["-m", markers])
|
|
26
|
+
|
|
27
|
+
if test_pattern:
|
|
28
|
+
cmd.extend(["-k", test_pattern])
|
|
29
|
+
|
|
30
|
+
# Add the test file
|
|
31
|
+
cmd.append("test_standalone_toolkit.py")
|
|
32
|
+
|
|
33
|
+
print(f"Running: {' '.join(cmd)}")
|
|
34
|
+
return subprocess.run(cmd).returncode
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
parser = argparse.ArgumentParser(description="Run standalone tools test suite")
|
|
39
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"-c", "--coverage", action="store_true", help="Run with coverage analysis"
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument("-k", "--pattern", help="Run tests matching pattern")
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"-m", "--markers", help="Run tests with specific markers (e.g., 'not slow')"
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--install-deps", action="store_true", help="Install test dependencies first"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
args = parser.parse_args()
|
|
52
|
+
|
|
53
|
+
if args.install_deps:
|
|
54
|
+
print("Installing test dependencies...")
|
|
55
|
+
subprocess.run([sys.executable, "-m", "pip", "install", "pytest", "pytest-cov"])
|
|
56
|
+
|
|
57
|
+
return run_tests(
|
|
58
|
+
test_pattern=args.pattern,
|
|
59
|
+
verbose=args.verbose,
|
|
60
|
+
coverage=args.coverage,
|
|
61
|
+
markers=args.markers,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
sys.exit(main())
|