kopipasta 0.3.0__py3-none-any.whl → 0.5.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 kopipasta might be problematic. Click here for more details.
- kopipasta/main.py +291 -12
- {kopipasta-0.3.0.dist-info → kopipasta-0.5.0.dist-info}/METADATA +2 -1
- kopipasta-0.5.0.dist-info/RECORD +8 -0
- kopipasta-0.3.0.dist-info/RECORD +0 -8
- {kopipasta-0.3.0.dist-info → kopipasta-0.5.0.dist-info}/LICENSE +0 -0
- {kopipasta-0.3.0.dist-info → kopipasta-0.5.0.dist-info}/WHEEL +0 -0
- {kopipasta-0.3.0.dist-info → kopipasta-0.5.0.dist-info}/entry_points.txt +0 -0
- {kopipasta-0.3.0.dist-info → kopipasta-0.5.0.dist-info}/top_level.txt +0 -0
kopipasta/main.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import os
|
|
3
3
|
import argparse
|
|
4
|
+
import ast
|
|
4
5
|
import re
|
|
6
|
+
from textwrap import dedent
|
|
5
7
|
import pyperclip
|
|
6
8
|
import fnmatch
|
|
7
9
|
|
|
@@ -88,6 +90,7 @@ def get_language_for_file(file_path):
|
|
|
88
90
|
'.ts': 'typescript',
|
|
89
91
|
'.tsx': 'tsx',
|
|
90
92
|
'.html': 'html',
|
|
93
|
+
'.htm': 'html',
|
|
91
94
|
'.css': 'css',
|
|
92
95
|
'.json': 'json',
|
|
93
96
|
'.md': 'markdown',
|
|
@@ -96,10 +99,251 @@ def get_language_for_file(file_path):
|
|
|
96
99
|
'.yml': 'yaml',
|
|
97
100
|
'.yaml': 'yaml',
|
|
98
101
|
'.go': 'go',
|
|
99
|
-
'.toml': 'toml'
|
|
102
|
+
'.toml': 'toml',
|
|
103
|
+
'.c': 'c',
|
|
104
|
+
'.cpp': 'cpp',
|
|
105
|
+
'.cc': 'cpp',
|
|
106
|
+
'.h': 'cpp',
|
|
107
|
+
'.hpp': 'cpp',
|
|
100
108
|
}
|
|
101
109
|
return language_map.get(extension, '')
|
|
102
110
|
|
|
111
|
+
def split_python_file(file_content):
|
|
112
|
+
"""
|
|
113
|
+
Splits Python code into logical chunks using the AST module.
|
|
114
|
+
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
115
|
+
"""
|
|
116
|
+
tree = ast.parse(file_content)
|
|
117
|
+
chunks = []
|
|
118
|
+
prev_end = 0
|
|
119
|
+
lines = file_content.splitlines(keepends=True)
|
|
120
|
+
|
|
121
|
+
def get_code(start, end):
|
|
122
|
+
return ''.join(lines[start:end])
|
|
123
|
+
|
|
124
|
+
for node in ast.iter_child_nodes(tree):
|
|
125
|
+
if hasattr(node, 'lineno'):
|
|
126
|
+
start_line = node.lineno - 1 # Convert to 0-indexed
|
|
127
|
+
end_line = getattr(node, 'end_lineno', None)
|
|
128
|
+
if end_line is None:
|
|
129
|
+
end_line = node.lineno
|
|
130
|
+
# Add code before the node
|
|
131
|
+
if prev_end < start_line:
|
|
132
|
+
chunks.append((get_code(prev_end, start_line), prev_end, start_line))
|
|
133
|
+
# Add the node code
|
|
134
|
+
chunks.append((get_code(start_line, end_line), start_line, end_line))
|
|
135
|
+
prev_end = end_line
|
|
136
|
+
# Add any remaining code at the end
|
|
137
|
+
if prev_end < len(lines):
|
|
138
|
+
chunks.append((get_code(prev_end, len(lines)), prev_end, len(lines)))
|
|
139
|
+
return chunks
|
|
140
|
+
|
|
141
|
+
def split_javascript_file(file_content):
|
|
142
|
+
"""
|
|
143
|
+
Splits JavaScript code into logical chunks using regular expressions.
|
|
144
|
+
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
145
|
+
"""
|
|
146
|
+
lines = file_content.splitlines(keepends=True)
|
|
147
|
+
chunks = []
|
|
148
|
+
pattern = re.compile(
|
|
149
|
+
r'^\s*(export\s+)?(async\s+)?(function\s+\w+|class\s+\w+|\w+\s*=\s*\(.*?\)\s*=>)',
|
|
150
|
+
re.MULTILINE
|
|
151
|
+
)
|
|
152
|
+
matches = list(pattern.finditer(file_content))
|
|
153
|
+
|
|
154
|
+
if not matches:
|
|
155
|
+
return [(file_content, 0, len(lines))]
|
|
156
|
+
|
|
157
|
+
prev_end_line = 0
|
|
158
|
+
for match in matches:
|
|
159
|
+
start_index = match.start()
|
|
160
|
+
start_line = file_content.count('\n', 0, start_index)
|
|
161
|
+
if prev_end_line < start_line:
|
|
162
|
+
code = ''.join(lines[prev_end_line:start_line])
|
|
163
|
+
chunks.append((code, prev_end_line, start_line))
|
|
164
|
+
|
|
165
|
+
function_code_lines = []
|
|
166
|
+
brace_count = 0
|
|
167
|
+
in_block = False
|
|
168
|
+
for i in range(start_line, len(lines)):
|
|
169
|
+
line = lines[i]
|
|
170
|
+
function_code_lines.append(line)
|
|
171
|
+
brace_count += line.count('{') - line.count('}')
|
|
172
|
+
if '{' in line:
|
|
173
|
+
in_block = True
|
|
174
|
+
if in_block and brace_count == 0:
|
|
175
|
+
end_line = i + 1
|
|
176
|
+
code = ''.join(function_code_lines)
|
|
177
|
+
chunks.append((code, start_line, end_line))
|
|
178
|
+
prev_end_line = end_line
|
|
179
|
+
break
|
|
180
|
+
else:
|
|
181
|
+
end_line = len(lines)
|
|
182
|
+
code = ''.join(function_code_lines)
|
|
183
|
+
chunks.append((code, start_line, end_line))
|
|
184
|
+
prev_end_line = end_line
|
|
185
|
+
|
|
186
|
+
if prev_end_line < len(lines):
|
|
187
|
+
code = ''.join(lines[prev_end_line:])
|
|
188
|
+
chunks.append((code, prev_end_line, len(lines)))
|
|
189
|
+
|
|
190
|
+
return chunks
|
|
191
|
+
|
|
192
|
+
def split_html_file(file_content):
|
|
193
|
+
"""
|
|
194
|
+
Splits HTML code into logical chunks based on top-level elements using regular expressions.
|
|
195
|
+
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
196
|
+
"""
|
|
197
|
+
pattern = re.compile(r'<(?P<tag>\w+)(\s|>).*?</(?P=tag)>', re.DOTALL)
|
|
198
|
+
lines = file_content.splitlines(keepends=True)
|
|
199
|
+
chunks = []
|
|
200
|
+
matches = list(pattern.finditer(file_content))
|
|
201
|
+
|
|
202
|
+
if not matches:
|
|
203
|
+
return [(file_content, 0, len(lines))]
|
|
204
|
+
|
|
205
|
+
prev_end = 0
|
|
206
|
+
for match in matches:
|
|
207
|
+
start_index = match.start()
|
|
208
|
+
end_index = match.end()
|
|
209
|
+
start_line = file_content.count('\n', 0, start_index)
|
|
210
|
+
end_line = file_content.count('\n', 0, end_index)
|
|
211
|
+
|
|
212
|
+
if prev_end < start_line:
|
|
213
|
+
code = ''.join(lines[prev_end:start_line])
|
|
214
|
+
chunks.append((code, prev_end, start_line))
|
|
215
|
+
|
|
216
|
+
code = ''.join(lines[start_line:end_line])
|
|
217
|
+
chunks.append((code, start_line, end_line))
|
|
218
|
+
prev_end = end_line
|
|
219
|
+
|
|
220
|
+
if prev_end < len(lines):
|
|
221
|
+
code = ''.join(lines[prev_end:])
|
|
222
|
+
chunks.append((code, prev_end, len(lines)))
|
|
223
|
+
|
|
224
|
+
return chunks
|
|
225
|
+
|
|
226
|
+
def split_c_file(file_content):
|
|
227
|
+
"""
|
|
228
|
+
Splits C/C++ code into logical chunks using regular expressions.
|
|
229
|
+
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
230
|
+
"""
|
|
231
|
+
pattern = re.compile(r'^\s*(?:[\w\*\s]+)\s+(\w+)\s*\([^)]*\)\s*\{', re.MULTILINE)
|
|
232
|
+
lines = file_content.splitlines(keepends=True)
|
|
233
|
+
chunks = []
|
|
234
|
+
matches = list(pattern.finditer(file_content))
|
|
235
|
+
|
|
236
|
+
if not matches:
|
|
237
|
+
return [(file_content, 0, len(lines))]
|
|
238
|
+
|
|
239
|
+
prev_end_line = 0
|
|
240
|
+
for match in matches:
|
|
241
|
+
start_index = match.start()
|
|
242
|
+
start_line = file_content.count('\n', 0, start_index)
|
|
243
|
+
if prev_end_line < start_line:
|
|
244
|
+
code = ''.join(lines[prev_end_line:start_line])
|
|
245
|
+
chunks.append((code, prev_end_line, start_line))
|
|
246
|
+
|
|
247
|
+
function_code_lines = []
|
|
248
|
+
brace_count = 0
|
|
249
|
+
in_function = False
|
|
250
|
+
for i in range(start_line, len(lines)):
|
|
251
|
+
line = lines[i]
|
|
252
|
+
function_code_lines.append(line)
|
|
253
|
+
brace_count += line.count('{') - line.count('}')
|
|
254
|
+
if '{' in line:
|
|
255
|
+
in_function = True
|
|
256
|
+
if in_function and brace_count == 0:
|
|
257
|
+
end_line = i + 1
|
|
258
|
+
code = ''.join(function_code_lines)
|
|
259
|
+
chunks.append((code, start_line, end_line))
|
|
260
|
+
prev_end_line = end_line
|
|
261
|
+
break
|
|
262
|
+
else:
|
|
263
|
+
end_line = len(lines)
|
|
264
|
+
code = ''.join(function_code_lines)
|
|
265
|
+
chunks.append((code, start_line, end_line))
|
|
266
|
+
prev_end_line = end_line
|
|
267
|
+
|
|
268
|
+
if prev_end_line < len(lines):
|
|
269
|
+
code = ''.join(lines[prev_end_line:])
|
|
270
|
+
chunks.append((code, prev_end_line, len(lines)))
|
|
271
|
+
|
|
272
|
+
return chunks
|
|
273
|
+
|
|
274
|
+
def split_generic_file(file_content):
|
|
275
|
+
"""
|
|
276
|
+
Splits generic text files into chunks based on double newlines.
|
|
277
|
+
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
278
|
+
"""
|
|
279
|
+
lines = file_content.splitlines(keepends=True)
|
|
280
|
+
chunks = []
|
|
281
|
+
start = 0
|
|
282
|
+
for i, line in enumerate(lines):
|
|
283
|
+
if line.strip() == '':
|
|
284
|
+
if start < i:
|
|
285
|
+
chunk_code = ''.join(lines[start:i])
|
|
286
|
+
chunks.append((chunk_code, start, i))
|
|
287
|
+
start = i + 1
|
|
288
|
+
if start < len(lines):
|
|
289
|
+
chunk_code = ''.join(lines[start:])
|
|
290
|
+
chunks.append((chunk_code, start, len(lines)))
|
|
291
|
+
return chunks
|
|
292
|
+
|
|
293
|
+
def select_file_patches(file_path):
|
|
294
|
+
file_content = read_file_contents(file_path)
|
|
295
|
+
language = get_language_for_file(file_path)
|
|
296
|
+
chunks = []
|
|
297
|
+
total_char_count = 0
|
|
298
|
+
|
|
299
|
+
if language == 'python':
|
|
300
|
+
code_chunks = split_python_file(file_content)
|
|
301
|
+
elif language == 'javascript':
|
|
302
|
+
code_chunks = split_javascript_file(file_content)
|
|
303
|
+
elif language == 'html':
|
|
304
|
+
code_chunks = split_html_file(file_content)
|
|
305
|
+
elif language in ['c', 'cpp']:
|
|
306
|
+
code_chunks = split_c_file(file_content)
|
|
307
|
+
else:
|
|
308
|
+
code_chunks = split_generic_file(file_content)
|
|
309
|
+
|
|
310
|
+
print(f"\nSelecting patches for {file_path}")
|
|
311
|
+
for index, (chunk_code, start_line, end_line) in enumerate(code_chunks):
|
|
312
|
+
print(f"\nChunk {index + 1} (Lines {start_line + 1}-{end_line}):")
|
|
313
|
+
print(f"```{language}\n{chunk_code}\n```")
|
|
314
|
+
while True:
|
|
315
|
+
choice = input("(y)es include / (n)o skip / (q)uit rest of file? ").lower()
|
|
316
|
+
if choice == 'y':
|
|
317
|
+
chunks.append(chunk_code)
|
|
318
|
+
total_char_count += len(chunk_code)
|
|
319
|
+
break
|
|
320
|
+
elif choice == 'n':
|
|
321
|
+
placeholder = get_placeholder_comment(language)
|
|
322
|
+
chunks.append(placeholder)
|
|
323
|
+
total_char_count += len(placeholder)
|
|
324
|
+
break
|
|
325
|
+
elif choice == 'q':
|
|
326
|
+
print("Skipping the rest of the file.")
|
|
327
|
+
return chunks, total_char_count
|
|
328
|
+
else:
|
|
329
|
+
print("Invalid choice. Please enter 'y', 'n', or 'q'.")
|
|
330
|
+
|
|
331
|
+
return chunks, total_char_count
|
|
332
|
+
|
|
333
|
+
def get_placeholder_comment(language):
|
|
334
|
+
comments = {
|
|
335
|
+
'python': '# Skipped content\n',
|
|
336
|
+
'javascript': '// Skipped content\n',
|
|
337
|
+
'typescript': '// Skipped content\n',
|
|
338
|
+
'java': '// Skipped content\n',
|
|
339
|
+
'c': '// Skipped content\n',
|
|
340
|
+
'cpp': '// Skipped content\n',
|
|
341
|
+
'html': '<!-- Skipped content -->\n',
|
|
342
|
+
'css': '/* Skipped content */\n',
|
|
343
|
+
'default': '# Skipped content\n'
|
|
344
|
+
}
|
|
345
|
+
return comments.get(language, comments['default'])
|
|
346
|
+
|
|
103
347
|
def get_file_snippet(file_path, max_lines=50, max_bytes=4096):
|
|
104
348
|
snippet = ""
|
|
105
349
|
byte_count = 0
|
|
@@ -166,7 +410,7 @@ def select_files_in_directory(directory, ignore_patterns, current_char_count=0):
|
|
|
166
410
|
while True:
|
|
167
411
|
if current_char_count > 0:
|
|
168
412
|
print_char_count(current_char_count)
|
|
169
|
-
file_choice = input(f"{file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) (y/n/q)? ").lower()
|
|
413
|
+
file_choice = input(f"{file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) (y/n/p/q)? ").lower()
|
|
170
414
|
if file_choice == 'y':
|
|
171
415
|
if is_large_file(file_path):
|
|
172
416
|
while True:
|
|
@@ -186,11 +430,17 @@ def select_files_in_directory(directory, ignore_patterns, current_char_count=0):
|
|
|
186
430
|
break
|
|
187
431
|
elif file_choice == 'n':
|
|
188
432
|
break
|
|
433
|
+
elif file_choice == 'p':
|
|
434
|
+
chunks, char_count = select_file_patches(file_path)
|
|
435
|
+
if chunks:
|
|
436
|
+
selected_files.append((file_path, False, chunks))
|
|
437
|
+
current_char_count += char_count
|
|
438
|
+
break
|
|
189
439
|
elif file_choice == 'q':
|
|
190
440
|
print(f"Quitting selection for {directory}")
|
|
191
441
|
return selected_files, current_char_count
|
|
192
442
|
else:
|
|
193
|
-
print("Invalid choice. Please enter 'y', 'n', or 'q'.")
|
|
443
|
+
print("Invalid choice. Please enter 'y', 'n', 'p', or 'q'.")
|
|
194
444
|
print(f"Added {len(selected_files)} files from {directory}")
|
|
195
445
|
return selected_files, current_char_count
|
|
196
446
|
elif choice == 'q':
|
|
@@ -277,10 +527,22 @@ def generate_prompt(files_to_include, ignore_patterns, web_contents, env_vars):
|
|
|
277
527
|
prompt += get_project_structure(ignore_patterns)
|
|
278
528
|
prompt += "\n```\n\n"
|
|
279
529
|
prompt += "## File Contents\n\n"
|
|
280
|
-
for
|
|
530
|
+
for file_tuple in files_to_include:
|
|
531
|
+
if len(file_tuple) == 3:
|
|
532
|
+
file, use_snippet, chunks = file_tuple
|
|
533
|
+
else:
|
|
534
|
+
file, use_snippet = file_tuple
|
|
535
|
+
chunks = None
|
|
536
|
+
|
|
281
537
|
relative_path = get_relative_path(file)
|
|
282
538
|
language = get_language_for_file(file)
|
|
283
|
-
|
|
539
|
+
|
|
540
|
+
if chunks is not None:
|
|
541
|
+
prompt += f"### {relative_path} (selected patches)\n\n```{language}\n"
|
|
542
|
+
for chunk in chunks:
|
|
543
|
+
prompt += f"{chunk}\n"
|
|
544
|
+
prompt += "```\n\n"
|
|
545
|
+
elif use_snippet:
|
|
284
546
|
file_content = get_file_snippet(file)
|
|
285
547
|
prompt += f"### {relative_path} (snippet)\n\n```{language}\n{file_content}\n```\n\n"
|
|
286
548
|
else:
|
|
@@ -332,13 +594,30 @@ def main():
|
|
|
332
594
|
print(f"Added web content from: {input_path}")
|
|
333
595
|
elif os.path.isfile(input_path):
|
|
334
596
|
if not is_ignored(input_path, ignore_patterns) and not is_binary(input_path):
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
597
|
+
while True:
|
|
598
|
+
file_choice = input(f"{input_path} (y)es include / (n)o skip / (p)atches / (q)uit? ").lower()
|
|
599
|
+
if file_choice == 'y':
|
|
600
|
+
use_snippet = is_large_file(input_path)
|
|
601
|
+
files_to_include.append((input_path, use_snippet))
|
|
602
|
+
if use_snippet:
|
|
603
|
+
current_char_count += len(get_file_snippet(input_path))
|
|
604
|
+
else:
|
|
605
|
+
current_char_count += os.path.getsize(input_path)
|
|
606
|
+
print(f"Added file: {input_path}{' (snippet)' if use_snippet else ''}")
|
|
607
|
+
break
|
|
608
|
+
elif file_choice == 'n':
|
|
609
|
+
break
|
|
610
|
+
elif file_choice == 'p':
|
|
611
|
+
chunks, char_count = select_file_patches(input_path)
|
|
612
|
+
if chunks:
|
|
613
|
+
files_to_include.append((input_path, False, chunks))
|
|
614
|
+
current_char_count += char_count
|
|
615
|
+
break
|
|
616
|
+
elif file_choice == 'q':
|
|
617
|
+
print("Quitting.")
|
|
618
|
+
return
|
|
619
|
+
else:
|
|
620
|
+
print("Invalid choice. Please enter 'y', 'n', 'p', or 'q'.")
|
|
342
621
|
else:
|
|
343
622
|
print(f"Ignored file: {input_path}")
|
|
344
623
|
elif os.path.isdir(input_path):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: kopipasta
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A CLI tool to generate prompts with project structure and file contents
|
|
5
5
|
Home-page: https://github.com/mkorpela/kopipasta
|
|
6
6
|
Author: Mikko Korpela
|
|
@@ -20,6 +20,7 @@ Requires-Python: >=3.8
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: pyperclip==1.9.0
|
|
23
|
+
Requires-Dist: requests==2.32.3
|
|
23
24
|
|
|
24
25
|
# kopipasta
|
|
25
26
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
kopipasta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
kopipasta/main.py,sha256=E1YQkvGvZSZo6t7vjkRgc2YQINrCqttIFVOgUwCAGT4,26031
|
|
3
|
+
kopipasta-0.5.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
|
|
4
|
+
kopipasta-0.5.0.dist-info/METADATA,sha256=4X9EkW4d2pO3Zp6HCfBujdITqRWVMKh9SrvxldcqW0g,5431
|
|
5
|
+
kopipasta-0.5.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
6
|
+
kopipasta-0.5.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
|
|
7
|
+
kopipasta-0.5.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
|
|
8
|
+
kopipasta-0.5.0.dist-info/RECORD,,
|
kopipasta-0.3.0.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
kopipasta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
kopipasta/main.py,sha256=5-zAPtdF4PU5xbMFJk5x5lmVAAXSIQ0FoKko4znch3k,15829
|
|
3
|
-
kopipasta-0.3.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
|
|
4
|
-
kopipasta-0.3.0.dist-info/METADATA,sha256=rhI1wv4YH5aWoz1rVmLPsEvt1N0Vh_d9EHKr0PhckPM,5399
|
|
5
|
-
kopipasta-0.3.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
6
|
-
kopipasta-0.3.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
|
|
7
|
-
kopipasta-0.3.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
|
|
8
|
-
kopipasta-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|